From 6ab302e4a828be63e0daee620ab29c659e6698a9 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Mon, 15 May 2023 15:10:11 +0200 Subject: [PATCH 01/46] Rewrite of process engine to support actions in tasks resolves #205 and #207 (#237) * New process engine seems to work. Needs more tests and verification * refactored to make class more testable * added tests for ProcessEngine * Refactor and delete old and unused code * added tests for ProcessEventDispatcher * Add tests and fix ProcessNavigator * add available actions to currentTask and perform authcheck * action passed along from PUT process/next to gateway filters * fix bug in AppProcessState ctor * add fields for read/write and check users permissions * Fix test stub implementation of IProcessExclusiveGateway * Fixing some reported code smells * Some code smell fixes. Added logic to dispatch abandon event if action is reject * remove unfinished test file * add tests for method in ProcessClient * add test for classes extending storage classes * add test for null values in extensions * revert code changes due to test * add frontend feature and parse request body on process/next if present * add frontend feature and parse request body on process/next if present * fix codeql warning * add v8 as target of github workflows in addition to main * Fix return type of all methods in ProcessController returning ProcessState * Authorize action moved to AuthorizationClient TaskType is substituted with corresponding action earlier Resolvs #207 * Fixed some issues after review and added some more tests * fix codeQL warning and improve test * Fix some code smells * Update src/Altinn.App.Api/Controllers/InstancesController.cs Co-authored-by: Ronny Birkeli * fix build error --------- Co-authored-by: Ronny Birkeli --- .github/workflows/codeql.yml | 4 +- .github/workflows/dotnet-test.yml | 4 +- .github/workflows/test-and-analyze-fork.yml | 2 +- .github/workflows/test-and-analyze.yml | 4 +- .../Controllers/InstancesController.cs | 87 +- .../Controllers/ProcessController.cs | 224 ++-- src/Altinn.App.Api/Models/AppProcessState.cs | 16 + src/Altinn.App.Api/Models/ProcessNext.cs | 15 + .../Extensions/InstanceEventExtensions.cs | 55 + .../Extensions/ProcessStateExtensions.cs | 39 + .../Extensions/ServiceCollectionExtensions.cs | 8 +- .../Features/IProcessExclusiveGateway.cs | 5 +- .../Authorization/AuthorizationClient.cs | 24 + .../Clients/Storage/ProcessClient.cs | 16 - .../Interface/IAuthorization.cs | 13 + src/Altinn.App.Core/Interface/IProcess.cs | 8 - .../Interface/IProcessChangeHandler.cs | 32 - .../Interface/IProcessEngine.cs | 26 - .../Internal/App/FrontendFeatures.cs | 1 + .../AltinnExtensionProperties/AltinnAction.cs | 16 + .../AltinnProperties.cs | 24 + .../Process/Elements/AppProcessElementInfo.cs | 52 + .../Process/Elements/AppProcessState.cs | 44 + .../Process/Elements/ConfirmationTask.cs | 13 +- .../Internal/Process/Elements/DataTask.cs | 15 +- .../Internal/Process/Elements/ElementInfo.cs | 28 - .../Process/Elements/ExtensionElements.cs | 17 + .../Internal/Process/Elements/FeedbackTask.cs | 13 +- .../Internal/Process/Elements/ITask.cs | 7 +- .../Internal/Process/Elements/NullTask.cs | 7 +- .../Internal/Process/Elements/ProcessTask.cs | 6 + .../Internal/Process/Elements/TaskBase.cs | 7 +- .../Internal/Process/IFlowHydration.cs | 29 - .../Internal/Process/IProcessEngine.cs | 29 + .../Process/IProcessEventDispatcher.cs | 23 + .../Internal/Process/IProcessNavigator.cs | 20 + .../Internal/Process/IProcessReader.cs | 34 +- .../Internal/Process/ProcessChangeHandler.cs | 449 -------- .../Internal/Process/ProcessEngine.cs | 345 +++++-- .../Process/ProcessEventDispatcher.cs | 157 +++ .../Internal/Process/ProcessException.cs | 17 - .../{FlowHydration.cs => ProcessNavigator.cs} | 47 +- .../Internal/Process/ProcessNextRequest.cs | 23 + .../Internal/Process/ProcessReader.cs | 110 +- .../Internal/Process/ProcessStartRequest.cs | 31 + .../Models/ProcessChangeContext.cs | 76 -- .../Models/ProcessChangeInfo.cs | 18 - .../Models/ProcessChangeResult.cs | 37 + .../Models/ProcessStateChange.cs | 6 +- ...nstancesController_ActiveInstancesTests.cs | 1 + .../InstancesController_CopyInstanceTests.cs | 8 +- .../Mocks/AuthorizationMock.cs | 14 +- .../InstanceEventExtensionsTests.cs | 215 ++++ .../Extensions/ProcessStateExtensionTests.cs | 67 ++ .../Clients/EventsSubscriptionClientTests.cs | 8 + .../Internal/App/AppMedataTest.cs | 6 +- .../Internal/App/FrontendFeaturesTest.cs | 3 +- .../Process/Elements/AppProcessStateTests.cs | 132 +++ .../Internal/Process/FlowHydrationTests.cs | 265 ----- .../Internal/Process/ProcessEngineTest.cs | 970 ++++++++++++++++-- .../Process/ProcessEventDispatcherTests.cs | 823 +++++++++++++++ .../Internal/Process/ProcessNavigatorTests.cs | 190 ++++ .../Internal/Process/ProcessReaderTests.cs | 302 ++---- .../StubGatewayFilters/DataValuesFilter.cs | 3 +- .../config/process/process.bpmn | 10 +- .../TestData/simple-gateway-default.bpmn | 35 +- .../simple-gateway-with-join-gateway.bpmn | 44 +- .../Process/TestData/simple-gateway.bpmn | 35 +- .../Process/TestData/simple-linear-both.bpmn | 46 + .../Process/TestData/simple-linear-new.bpmn | 46 + .../Process/TestData/simple-no-end.bpmn | 14 +- .../Process/TestUtils/ProcessTestUtils.cs | 5 +- 72 files changed, 3809 insertions(+), 1716 deletions(-) create mode 100644 src/Altinn.App.Api/Models/AppProcessState.cs create mode 100644 src/Altinn.App.Api/Models/ProcessNext.cs create mode 100644 src/Altinn.App.Core/Extensions/InstanceEventExtensions.cs create mode 100644 src/Altinn.App.Core/Extensions/ProcessStateExtensions.cs delete mode 100644 src/Altinn.App.Core/Interface/IProcessChangeHandler.cs delete mode 100644 src/Altinn.App.Core/Interface/IProcessEngine.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnProperties.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AppProcessState.cs delete mode 100644 src/Altinn.App.Core/Internal/Process/Elements/ElementInfo.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs delete mode 100644 src/Altinn.App.Core/Internal/Process/IFlowHydration.cs create mode 100644 src/Altinn.App.Core/Internal/Process/IProcessEngine.cs create mode 100644 src/Altinn.App.Core/Internal/Process/IProcessEventDispatcher.cs create mode 100644 src/Altinn.App.Core/Internal/Process/IProcessNavigator.cs delete mode 100644 src/Altinn.App.Core/Internal/Process/ProcessChangeHandler.cs create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs rename src/Altinn.App.Core/Internal/Process/{FlowHydration.cs => ProcessNavigator.cs} (59%) create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessNextRequest.cs create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessStartRequest.cs delete mode 100644 src/Altinn.App.Core/Models/ProcessChangeContext.cs delete mode 100644 src/Altinn.App.Core/Models/ProcessChangeInfo.cs create mode 100644 src/Altinn.App.Core/Models/ProcessChangeResult.cs create mode 100644 test/Altinn.App.Core.Tests/Extensions/InstanceEventExtensionsTests.cs create mode 100644 test/Altinn.App.Core.Tests/Extensions/ProcessStateExtensionTests.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/Elements/AppProcessStateTests.cs delete mode 100644 test/Altinn.App.Core.Tests/Internal/Process/FlowHydrationTests.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-both.bpmn create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-new.bpmn diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e9357d43d..9128935e1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,10 +2,10 @@ name: "CodeQL" on: push: - branches: [ "main" ] + branches: [ "main", "v8" ] pull_request: # The branches below must be a subset of the branches above - branches: [ "main" ] + branches: [ "main", "v8" ] schedule: - cron: '37 20 * * 3' diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index 56849711f..4b89aa166 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -1,9 +1,9 @@ name: Build and Test on windows, macos and ubuntu on: push: - branches: [ main ] + branches: [ main, v8 ] pull_request: - branches: [ main ] + branches: [ main, v8 ] types: [opened, synchronize, reopened] workflow_dispatch: jobs: diff --git a/.github/workflows/test-and-analyze-fork.yml b/.github/workflows/test-and-analyze-fork.yml index 8c6b65433..d68ce6cc3 100644 --- a/.github/workflows/test-and-analyze-fork.yml +++ b/.github/workflows/test-and-analyze-fork.yml @@ -1,7 +1,7 @@ name: Code test and analysis (fork) on: pull_request: - branches: [ main ] + branches: [ main, v8 ] types: [opened, synchronize, reopened, ready_for_review] jobs: test: diff --git a/.github/workflows/test-and-analyze.yml b/.github/workflows/test-and-analyze.yml index 6d5714280..7dcd477f4 100644 --- a/.github/workflows/test-and-analyze.yml +++ b/.github/workflows/test-and-analyze.yml @@ -1,9 +1,9 @@ name: Code test and analysis on: push: - branches: [ main ] + branches: [ main, v8 ] pull_request: - branches: [ main ] + branches: [ main, v8 ] types: [opened, synchronize, reopened] workflow_dispatch: jobs: diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index aec29404d..2de5fffc0 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -2,7 +2,6 @@ using System.Net; using System.Text; - using Altinn.App.Api.Helpers.RequestHandling; using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Mappers; @@ -16,6 +15,7 @@ using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; using Altinn.Authorization.ABAC.Xacml.JsonProfile; @@ -25,12 +25,10 @@ using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; - using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; - using Newtonsoft.Json; namespace Altinn.App.Api.Controllers @@ -60,8 +58,8 @@ public class InstancesController : ControllerBase private readonly IInstantiationValidator _instantiationValidator; private readonly IPDP _pdp; private readonly IPrefill _prefillService; - private readonly IProcessEngine _processEngine; private readonly AppSettings _appSettings; + private readonly IProcessEngine _processEngine; private const long RequestSizeLimit = 2000 * 1024 * 1024; @@ -81,7 +79,7 @@ public InstancesController( IEvents eventsService, IOptions appSettings, IPrefill prefillService, - IProfile profileClient, + IProfile profileClient, IProcessEngine processEngine) { _logger = logger; @@ -208,6 +206,7 @@ public async Task> Post( } else { + // create minimum instance template instanceTemplate = new Instance { InstanceOwner = new InstanceOwner { PartyId = instanceOwnerPartyId.Value.ToString() } @@ -267,12 +266,24 @@ public async Task> Post( Instance instance; instanceTemplate.Process = null; - ProcessChangeContext processChangeContext = new ProcessChangeContext(instanceTemplate, User); + ProcessStateChange? change = null; + try { - // start process - processChangeContext.DontUpdateProcessAndDispatchEvents = true; - processChangeContext = await _processEngine.StartProcess(processChangeContext); + // start process and goto next task + ProcessStartRequest processStartRequest = new ProcessStartRequest + { + Instance = instanceTemplate, + User = User, + Dryrun = true + }; + var result = await _processEngine.StartProcess(processStartRequest); + if (!result.Success) + { + return Conflict(result.ErrorMessage); + } + + change = result.ProcessStateChange; // create the instance instance = await _instanceClient.CreateInstance(org, app, instanceTemplate); @@ -290,9 +301,14 @@ 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 - processChangeContext.Instance = instance; - processChangeContext.DontUpdateProcessAndDispatchEvents = false; - await _processEngine.StartTask(processChangeContext); + 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); } catch (Exception exception) { @@ -404,15 +420,21 @@ public async Task> PostSimplified( } Instance instance; + ProcessChangeResult processResult; try { + // start process and goto next task instanceTemplate.Process = null; - // start process - ProcessChangeContext processChangeContext = new ProcessChangeContext(instanceTemplate, User); - processChangeContext.Prefill = instansiationInstance.Prefill; - processChangeContext.DontUpdateProcessAndDispatchEvents = true; - processChangeContext = await _processEngine.StartProcess(processChangeContext); + var request = new ProcessStartRequest() + { + Instance = instanceTemplate, + User = User, + Dryrun = true, + Prefill = instansiationInstance.Prefill + }; + + processResult = await _processEngine.StartProcess(request); Instance? source = null; @@ -445,9 +467,14 @@ public async Task> PostSimplified( instance = await _instanceClient.GetInstance(instance); - processChangeContext.Instance = instance; - processChangeContext.DontUpdateProcessAndDispatchEvents = false; - await _processEngine.StartTask(processChangeContext); + var updateRequest = new ProcessStartRequest() + { + Instance = instance, + User = User, + Dryrun = false, + Prefill = instansiationInstance.Prefill + }; + await _processEngine.UpdateInstanceAndRerunEvents(updateRequest, processResult.ProcessStateChange?.Events); } catch (Exception exception) { @@ -535,12 +562,14 @@ public async Task CopyInstance( { return StatusCode((int)HttpStatusCode.Forbidden, validationResult); } - - ProcessChangeContext processChangeContext = new(targetInstance, User) + + ProcessStartRequest processStartRequest = new() { - DontUpdateProcessAndDispatchEvents = true + Instance = targetInstance, + User = User, + Dryrun = true }; - processChangeContext = await _processEngine.StartProcess(processChangeContext); + var startResult = await _processEngine.StartProcess(processStartRequest); targetInstance = await _instanceClient.CreateInstance(org, app, targetInstance); @@ -548,9 +577,13 @@ public async Task CopyInstance( targetInstance = await _instanceClient.GetInstance(targetInstance); - processChangeContext.Instance = targetInstance; - processChangeContext.DontUpdateProcessAndDispatchEvents = false; - await _processEngine.StartTask(processChangeContext); + ProcessStartRequest rerunRequest = new() + { + Instance = targetInstance, + Dryrun = false, + User = User + }; + await _processEngine.UpdateInstanceAndRerunEvents(rerunRequest, 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 6d6926e83..6d6bdba3e 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -1,29 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Threading.Tasks; - using Altinn.App.Api.Infrastructure.Filters; +using Altinn.App.Api.Models; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; -using Altinn.Authorization.ABAC.Xacml.JsonProfile; -using Altinn.Common.PEP.Helpers; -using Altinn.Common.PEP.Interfaces; using Altinn.Platform.Storage.Interface.Models; - using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; +using AppProcessState = Altinn.App.Core.Internal.Process.Elements.AppProcessState; namespace Altinn.App.Api.Controllers { @@ -42,10 +32,9 @@ public class ProcessController : ControllerBase private readonly IInstance _instanceClient; private readonly IProcess _processService; private readonly IValidation _validationService; - private readonly IPDP _pdp; + private readonly IAuthorization _authorization; private readonly IProcessEngine _processEngine; private readonly IProcessReader _processReader; - private readonly IFlowHydration _flowHydration; /// /// Initializes a new instance of the @@ -55,19 +44,17 @@ public ProcessController( IInstance instanceClient, IProcess processService, IValidation validationService, - IPDP pdp, - IProcessEngine processEngine, + IAuthorization authorization, IProcessReader processReader, - IFlowHydration flowHydration) + IProcessEngine processEngine) { _logger = logger; _instanceClient = instanceClient; _processService = processService; _validationService = validationService; - _pdp = pdp; - _processEngine = processEngine; + _authorization = authorization; _processReader = processReader; - _flowHydration = flowHydration; + _processEngine = processEngine; } /// @@ -82,7 +69,7 @@ public ProcessController( [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Authorize(Policy = "InstanceRead")] - public async Task> GetProcessState( + public async Task> GetProcessState( [FromRoute] string org, [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, @@ -91,9 +78,9 @@ public async Task> GetProcessState( try { Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - ProcessState processState = instance.Process; + AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, instance.Process); - return Ok(processState); + return Ok(appProcessState); } catch (PlatformHttpException e) { @@ -120,7 +107,7 @@ public async Task> GetProcessState( [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] [Authorize(Policy = "InstanceInstantiate")] - public async Task> StartProcess( + public async Task> StartProcess( [FromRoute] string org, [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, @@ -133,15 +120,21 @@ public async Task> StartProcess( { instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - ProcessChangeContext changeContext = new ProcessChangeContext(instance, User); - changeContext.RequestedProcessElementId = startEvent; - changeContext = await _processEngine.StartProcess(changeContext); - if (changeContext.FailedProcessChange) + var request = new ProcessStartRequest() { - return Conflict(changeContext.ProcessMessages[0].Message); + Instance = instance, + StartEventId = startEvent, + User = User, + Dryrun = false + }; + var result = await _processEngine.StartProcess(request); + if (!result.Success) + { + return Conflict(result.ErrorMessage); } - - return Ok(changeContext.Instance.Process); + + AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, result.ProcessStateChange?.NewProcessState); + return Ok(appProcessState); } catch (PlatformHttpException e) { @@ -167,6 +160,7 @@ public async Task> StartProcess( [HttpGet("next")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status409Conflict)] + [Obsolete("From v8 of nuget package navigation is done by sending performed action to the next api. Available actions are returned in the GET /process endpoint")] public async Task>> GetNextElements( [FromRoute] string org, [FromRoute] string app, @@ -191,10 +185,8 @@ public async Task>> GetNextElements( { return Conflict($"Instance does not have valid info about currentTask"); } - - List nextElements = await _flowHydration.NextFollowAndFilterGateways(instance, currentTaskId, false); - return Ok(nextElements.Select(e => e.Id).ToList()); + return Ok(new List()); } catch (PlatformHttpException e) { @@ -235,8 +227,7 @@ private async Task CanTaskBeEnded(Instance instance, string currentElement /// application identifier which is unique within an organisation /// unique id of the party that is the owner of the instance /// unique id to identify the instance - /// the id of the next element to move to. Query parameter is optional, - /// but must be specified if more than one element can be reached from the current process ellement. + /// obsolete: alias for action /// Optional parameter to pass on the language used in the form if this differs from the profile language, /// which otherwise is used automatically. The language is picked up when generating the PDF when leaving a step, /// and is not used for anything else. @@ -245,7 +236,7 @@ private async Task CanTaskBeEnded(Instance instance, string currentElement [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task> NextElement( + public async Task> NextElement( [FromRoute] string org, [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, @@ -255,6 +246,12 @@ public async Task> NextElement( { try { + ProcessNext? processNext = null; + if (Request.Body != null && Request.Body.CanRead) + { + processNext = await DeserializeFromStream(Request.Body); + } + Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); if (instance.Process == null) @@ -267,15 +264,6 @@ public async Task> NextElement( return Conflict($"Process is ended."); } - if (!string.IsNullOrEmpty(elementId)) - { - ElementInfo elemInfo = _processReader.GetElementInfo(elementId); - if (elemInfo == null) - { - return BadRequest($"Requested element id {elementId} is not found in process definition"); - } - } - string altinnTaskType = instance.Process.CurrentTask?.AltinnTaskType; if (altinnTaskType == null) @@ -283,41 +271,30 @@ public async Task> NextElement( return Conflict($"Instance does not have current altinn task type information!"); } - ProcessSequenceFlowType processSequenceFlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext; - List possibleNextElements = await _flowHydration.NextFollowAndFilterGateways(instance, instance.Process.CurrentTask?.ElementId, elementId.IsNullOrEmpty()); - string targetElement = ProcessHelper.GetValidNextElementOrError(elementId, possibleNextElements.Select(e => e.Id).ToList(), out ProcessError processError); - - if (!string.IsNullOrEmpty(elementId) && processError == null) - { - List flows = _processReader.GetSequenceFlowsBetween(instance.Process.CurrentTask?.ElementId, targetElement); - processSequenceFlowType = ProcessHelper.GetSequenceFlowType(flows); - } - bool authorized; - if (processSequenceFlowType.Equals(ProcessSequenceFlowType.CompleteCurrentMoveToNext)) - { - authorized = await AuthorizeAction(altinnTaskType, org, app, instanceOwnerPartyId, instanceGuid); - } - else - { - ElementInfo elemInfo = _processReader.GetElementInfo(targetElement); - authorized = await AuthorizeAction(elemInfo.AltinnTaskType, org, app, instanceOwnerPartyId, instanceGuid, elemInfo.Id); - } + string checkedAction = EnsureActionNotTaskType(processNext?.Action ?? altinnTaskType); + authorized = await AuthorizeAction(checkedAction, org, app, instanceOwnerPartyId, instanceGuid); if (!authorized) { return Forbid(); } - ProcessChangeContext changeContext = new ProcessChangeContext(instance, User); - changeContext.RequestedProcessElementId = elementId; - changeContext = await _processEngine.Next(changeContext); - if (changeContext.FailedProcessChange) + var request = new ProcessNextRequest() { - return Conflict(changeContext.ProcessMessages[0].Message); + Instance = instance, + User = User, + Action = checkedAction + }; + var result = await _processEngine.Next(request); + if (!result.Success) + { + return Conflict(result.ErrorMessage); } - return Ok(changeContext.Instance.Process); + AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, result.ProcessStateChange?.NewProcessState); + + return Ok(appProcessState); } catch (PlatformHttpException e) { @@ -342,7 +319,7 @@ public async Task> NextElement( [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task> CompleteProcess( + public async Task> CompleteProcess( [FromRoute] string org, [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, @@ -382,7 +359,7 @@ public async Task> CompleteProcess( int counter = 0; do { - string altinnTaskType = instance.Process.CurrentTask?.AltinnTaskType; + string altinnTaskType = EnsureActionNotTaskType(instance.Process.CurrentTask?.AltinnTaskType); bool authorized = await AuthorizeAction(altinnTaskType, org, app, instanceOwnerPartyId, instanceGuid); if (!authorized) @@ -395,27 +372,22 @@ public async Task> CompleteProcess( return Conflict($"Instance is not valid for task {currentTaskId}. Automatic completion of process is stopped"); } - List nextElements = await _flowHydration.NextFollowAndFilterGateways(instance, currentTaskId); - - if (nextElements.Count > 1) - { - return Conflict($"Cannot complete process. Multiple outgoing sequence flows detected from task {currentTaskId}. Please select manually among {nextElements}"); - } - - string nextElement = nextElements.First().Id; - try { - ProcessChangeContext processChange = new ProcessChangeContext(instance, User); - processChange.RequestedProcessElementId = nextElement; - processChange = await _processEngine.Next(processChange); + ProcessNextRequest request = new ProcessNextRequest() + { + Instance = instance, + User = User, + Action = altinnTaskType + }; + var result = await _processEngine.Next(request); - if (processChange.FailedProcessChange) + if (!result.Success) { - return Conflict(processChange.ProcessMessages[0].Message); + return Conflict(result.ErrorMessage); } - currentTaskId = instance.Process.CurrentTask?.ElementId; + currentTaskId = result.ProcessStateChange?.NewProcessState.CurrentTask.ElementId; } catch (Exception ex) { @@ -432,7 +404,8 @@ public async Task> CompleteProcess( return StatusCode(500, $"More than {counter} iterations detected in process. Possible loop. Fix app process definition!"); } - return Ok(instance.Process); + AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, instance.Process); + return Ok(appProcessState); } /// @@ -460,6 +433,28 @@ public async Task GetProcessHistory( return ExceptionResponse(processException, $"Unable to find retrieve process history for instance {instanceOwnerPartyId}/{instanceGuid}. Exception: {processException}"); } } + + private async Task ConvertAndAuthorizeActions(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, ProcessState? processState) + { + AppProcessState appProcessState = new AppProcessState(processState); + if (appProcessState.CurrentTask?.ElementId != null) + { + var flowElement = _processReader.GetFlowElement(appProcessState.CurrentTask.ElementId); + if (flowElement is ProcessTask processTask) + { + appProcessState.CurrentTask.Actions = new Dictionary(); + foreach (AltinnAction action in processTask.ExtensionElements?.AltinnProperties?.AltinnActions ?? new List()) + { + appProcessState.CurrentTask.Actions.Add(action.Id, await AuthorizeAction(action.Id, org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id)); + } + + appProcessState.CurrentTask.HasWriteAccess = await AuthorizeAction("write", org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id); + appProcessState.CurrentTask.HasReadAccess = await AuthorizeAction("read", org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id); + } + } + + return appProcessState; + } private ActionResult ExceptionResponse(Exception exception, string message) { @@ -479,34 +474,24 @@ private ActionResult ExceptionResponse(Exception exception, string message) return StatusCode(500, $"{message}"); } - private async Task AuthorizeAction(string currentTaskType, string org, string app, int instanceOwnerPartyId, Guid instanceGuid, string taskId = null) + private async Task AuthorizeAction(string action, string org, string app, int instanceOwnerPartyId, Guid instanceGuid, string taskId = null) { - string actionType; + return await _authorization.AuthorizeAction(new AppIdentifier(org, app), new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), HttpContext.User, action, taskId); + } - switch (currentTaskType) + private static string EnsureActionNotTaskType(string actionOrTaskType) + { + switch (actionOrTaskType) { case "data": case "feedback": - actionType = "write"; - break; + return "write"; case "confirmation": - actionType = "confirm"; - break; + return "confirm"; default: - actionType = currentTaskType; - break; + // Not any known task type, so assume it is an action type + return actionOrTaskType; } - - XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest(org, app, HttpContext.User, actionType, instanceOwnerPartyId, instanceGuid, taskId); - XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); - if (response?.Response == null) - { - _logger.LogInformation($"// Process Controller // Authorization of moving process forward failed with request: {JsonConvert.SerializeObject(request)}."); - return false; - } - - bool authorized = DecisionHelper.ValidatePdpDecision(response.Response, HttpContext.User); - return authorized; } private ActionResult HandlePlatformHttpException(PlatformHttpException e, string defaultMessage) @@ -515,18 +500,25 @@ private ActionResult HandlePlatformHttpException(PlatformHttpException e, string { return Forbid(); } - else if (e.Response.StatusCode == HttpStatusCode.NotFound) + + if (e.Response.StatusCode == HttpStatusCode.NotFound) { return NotFound(); } - else if (e.Response.StatusCode == HttpStatusCode.Conflict) + + if (e.Response.StatusCode == HttpStatusCode.Conflict) { return Conflict(); } - else - { - return ExceptionResponse(e, defaultMessage); - } + + return ExceptionResponse(e, defaultMessage); + } + + private static async Task DeserializeFromStream(Stream stream) + { + using StreamReader reader = new StreamReader(stream); + string text = await reader.ReadToEndAsync(); + return JsonConvert.DeserializeObject(text); } } } diff --git a/src/Altinn.App.Api/Models/AppProcessState.cs b/src/Altinn.App.Api/Models/AppProcessState.cs new file mode 100644 index 000000000..afc1ec4f5 --- /dev/null +++ b/src/Altinn.App.Api/Models/AppProcessState.cs @@ -0,0 +1,16 @@ +#nullable enable +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Api.Models; + +/// +/// Extended representation of a status object that holds the process state of an application instance. +/// The process is defined by the application's process specification BPMN file. +/// +public class AppProcessState: ProcessState +{ + /// + /// Actions that can be performed and if the user is allowed to perform them. + /// + public Dictionary? Actions { get; set; } +} diff --git a/src/Altinn.App.Api/Models/ProcessNext.cs b/src/Altinn.App.Api/Models/ProcessNext.cs new file mode 100644 index 000000000..675e9f841 --- /dev/null +++ b/src/Altinn.App.Api/Models/ProcessNext.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Altinn.App.Api.Models; + +/// +/// Model for process next body +/// +public class ProcessNext +{ + /// + /// Action performed + /// + [JsonPropertyName("action")] + public string? Action { get; set; } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Extensions/InstanceEventExtensions.cs b/src/Altinn.App.Core/Extensions/InstanceEventExtensions.cs new file mode 100644 index 000000000..bc6a54bc9 --- /dev/null +++ b/src/Altinn.App.Core/Extensions/InstanceEventExtensions.cs @@ -0,0 +1,55 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Extensions; + +/// +/// Extension methods for . +/// +public static class InstanceEventExtensions +{ + /// + /// Copies the values of the original to a new instance. + /// + /// The original . + /// New object with copies of values form original + public static InstanceEvent CopyValues(this InstanceEvent original) + { + return new InstanceEvent + { + Created = original.Created, + DataId = original.DataId, + EventType = original.EventType, + Id = original.Id, + InstanceId = original.InstanceId, + InstanceOwnerPartyId = original.InstanceOwnerPartyId, + ProcessInfo = new ProcessState + { + Started = original.ProcessInfo?.Started, + CurrentTask = new ProcessElementInfo + { + Flow = original.ProcessInfo?.CurrentTask?.Flow, + AltinnTaskType = original.ProcessInfo?.CurrentTask?.AltinnTaskType, + ElementId = original.ProcessInfo?.CurrentTask?.ElementId, + Name = original.ProcessInfo?.CurrentTask?.Name, + Started = original.ProcessInfo?.CurrentTask?.Started, + Ended = original.ProcessInfo?.CurrentTask?.Ended, + Validated = new ValidationStatus + { + CanCompleteTask = original.ProcessInfo?.CurrentTask?.Validated?.CanCompleteTask ?? false, + Timestamp = original.ProcessInfo?.CurrentTask?.Validated?.Timestamp + } + }, + + StartEvent = original.ProcessInfo?.StartEvent + }, + User = new PlatformUser + { + AuthenticationLevel = original.User.AuthenticationLevel, + EndUserSystemId = original.User.EndUserSystemId, + OrgId = original.User.OrgId, + UserId = original.User.UserId, + NationalIdentityNumber = original.User?.NationalIdentityNumber + } + }; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Extensions/ProcessStateExtensions.cs b/src/Altinn.App.Core/Extensions/ProcessStateExtensions.cs new file mode 100644 index 000000000..561e97505 --- /dev/null +++ b/src/Altinn.App.Core/Extensions/ProcessStateExtensions.cs @@ -0,0 +1,39 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Extensions; + +/// +/// Extension methods for +/// +public static class ProcessStateExtensions +{ + /// + /// Copies the values of the original to a new instance. + /// + /// The original . + /// New object with copies of values form original + public static ProcessState Copy(this ProcessState original) + { + ProcessState copyOfState = new ProcessState(); + + if (original.CurrentTask != null) + { + copyOfState.CurrentTask = new ProcessElementInfo(); + copyOfState.CurrentTask.FlowType = original.CurrentTask.FlowType; + copyOfState.CurrentTask.Name = original.CurrentTask.Name; + copyOfState.CurrentTask.Validated = original.CurrentTask.Validated; + copyOfState.CurrentTask.AltinnTaskType = original.CurrentTask.AltinnTaskType; + copyOfState.CurrentTask.Flow = original.CurrentTask.Flow; + copyOfState.CurrentTask.ElementId = original.CurrentTask.ElementId; + copyOfState.CurrentTask.Started = original.CurrentTask.Started; + copyOfState.CurrentTask.Ended = original.CurrentTask.Ended; + } + + copyOfState.EndEvent = original.EndEvent; + copyOfState.Started = original.Started; + copyOfState.Ended = original.Ended; + copyOfState.StartEvent = original.StartEvent; + + return copyOfState; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 91f9fb7f9..0d33707a6 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -36,6 +36,10 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Newtonsoft.Json.Linq; +using IProcessEngine = Altinn.App.Core.Internal.Process.IProcessEngine; +using IProcessReader = Altinn.App.Core.Internal.Process.IProcessReader; +using ProcessEngine = Altinn.App.Core.Internal.Process.ProcessEngine; +using ProcessReader = Altinn.App.Core.Internal.Process.ProcessReader; namespace Altinn.App.Core.Extensions { @@ -218,10 +222,10 @@ private static void AddAppOptions(IServiceCollection services) private static void AddProcessServices(IServiceCollection services) { services.TryAddTransient(); - services.TryAddTransient(); + services.TryAddTransient(); services.TryAddSingleton(); + services.TryAddTransient(); services.TryAddTransient(); - services.TryAddTransient(); } } } diff --git a/src/Altinn.App.Core/Features/IProcessExclusiveGateway.cs b/src/Altinn.App.Core/Features/IProcessExclusiveGateway.cs index 32c8d5095..4ea37a384 100644 --- a/src/Altinn.App.Core/Features/IProcessExclusiveGateway.cs +++ b/src/Altinn.App.Core/Features/IProcessExclusiveGateway.cs @@ -9,7 +9,7 @@ namespace Altinn.App.Core.Features; public interface IProcessExclusiveGateway { /// - /// + /// Id of the gateway in the BPMN process this filter applies to /// string GatewayId { get; } @@ -18,6 +18,7 @@ public interface IProcessExclusiveGateway /// /// Complete list of defined flows out of gateway /// Instance where process is about to move next + /// Action performed by the requester /// - public Task> FilterAsync(List outgoingFlows, Instance instance); + public Task> FilterAsync(List outgoingFlows, Instance instance, string? action); } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs index 03d7a37a9..9d3146a16 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs @@ -1,8 +1,13 @@ using System.Net.Http.Headers; +using System.Security.Claims; using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Interface; +using Altinn.App.Core.Models; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Helpers; +using Altinn.Common.PEP.Interfaces; using Altinn.Platform.Register.Models; using AltinnCore.Authentication.Utils; @@ -23,6 +28,7 @@ public class AuthorizationClient : IAuthorization private readonly IHttpContextAccessor _httpContextAccessor; private readonly AppSettings _settings; private readonly HttpClient _client; + private readonly IPDP _pdp; private readonly ILogger _logger; /// @@ -32,16 +38,19 @@ public class AuthorizationClient : IAuthorization /// the http context accessor. /// A Http client from the HttpClientFactory. /// The application settings. + /// /// the handler for logger service public AuthorizationClient( IOptions platformSettings, IHttpContextAccessor httpContextAccessor, HttpClient httpClient, IOptionsMonitor settings, + IPDP pdp, ILogger logger) { _httpContextAccessor = httpContextAccessor; _settings = settings.CurrentValue; + _pdp = pdp; _logger = logger; httpClient.BaseAddress = new Uri(platformSettings.Value.ApiAuthorizationEndpoint); httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); @@ -95,5 +104,20 @@ public AuthorizationClient( return result; } + + /// + public async Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null) + { + XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest(appIdentifier.Org, appIdentifier.App, user, action, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, taskId); + XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); + if (response?.Response == null) + { + _logger.LogWarning("Failed to get decision from pdp: {SerializeObject}", JsonConvert.SerializeObject(request)); + return false; + } + + bool authorized = DecisionHelper.ValidatePdpDecision(response.Response, user); + return authorized; + } } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs index 7d3486019..df905700f 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs @@ -20,7 +20,6 @@ public class ProcessClient : IProcess { private readonly AppSettings _appSettings; private readonly ILogger _logger; - private readonly IInstanceEvent _instanceEventClient; private readonly HttpClient _client; private readonly IHttpContextAccessor _httpContextAccessor; @@ -30,13 +29,11 @@ public class ProcessClient : IProcess public ProcessClient( IOptions platformSettings, IOptions appSettings, - IInstanceEvent instanceEventClient, ILogger logger, IHttpContextAccessor httpContextAccessor, HttpClient httpClient) { _appSettings = appSettings.Value; - _instanceEventClient = instanceEventClient; _httpContextAccessor = httpContextAccessor; _logger = logger; httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); @@ -82,18 +79,5 @@ public async Task GetProcessHistory(string instanceGuid, str throw await PlatformHttpException.CreateAsync(response); } - - /// - public async Task DispatchProcessEventsToStorage(Instance instance, List events) - { - string org = instance.Org; - string app = instance.AppId.Split("/")[1]; - - foreach (InstanceEvent instanceEvent in events) - { - instanceEvent.InstanceId = instance.Id; - await _instanceEventClient.SaveInstanceEvent(instanceEvent, org, app); - } - } } } diff --git a/src/Altinn.App.Core/Interface/IAuthorization.cs b/src/Altinn.App.Core/Interface/IAuthorization.cs index 282f1fe02..64dd95d89 100644 --- a/src/Altinn.App.Core/Interface/IAuthorization.cs +++ b/src/Altinn.App.Core/Interface/IAuthorization.cs @@ -1,3 +1,5 @@ +using System.Security.Claims; +using Altinn.App.Core.Models; using Altinn.Platform.Register.Models; namespace Altinn.App.Core.Interface @@ -21,5 +23,16 @@ public interface IAuthorization /// The party id. /// Boolean indicating whether or not the user can represent the selected party. Task ValidateSelectedParty(int userId, int partyId); + + /// + /// Check if the user is authorized to perform the given action on the given instance. + /// + /// + /// + /// + /// + /// + /// + Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null); } } diff --git a/src/Altinn.App.Core/Interface/IProcess.cs b/src/Altinn.App.Core/Interface/IProcess.cs index 92f98e3fa..582fa4ef4 100644 --- a/src/Altinn.App.Core/Interface/IProcess.cs +++ b/src/Altinn.App.Core/Interface/IProcess.cs @@ -13,14 +13,6 @@ public interface IProcess /// the stream Stream GetProcessDefinition(); - /// - /// Dispatches process events to storage. - /// - /// the instance - /// process events - /// - public Task DispatchProcessEventsToStorage(Instance instance, List events); - /// /// Gets the instance process events related to the instance matching the instance id. /// diff --git a/src/Altinn.App.Core/Interface/IProcessChangeHandler.cs b/src/Altinn.App.Core/Interface/IProcessChangeHandler.cs deleted file mode 100644 index 3fc247603..000000000 --- a/src/Altinn.App.Core/Interface/IProcessChangeHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Altinn.App.Core.Models; - -namespace Altinn.App.Core.Interface -{ - /// - /// Interface for Process Change Handler. Responsible for triggering events - /// - public interface IProcessChangeHandler - { - /// - /// Handle start of process - /// - Task HandleStart(ProcessChangeContext processChange); - - /// - /// Handle complete task and move to - /// - /// - Task HandleMoveToNext(ProcessChangeContext processChange); - - /// - /// Handle start task - /// - Task HandleStartTask(ProcessChangeContext processChange); - - /// - /// Check if current task can be completed - /// - /// - Task CanTaskBeEnded(ProcessChangeContext processChange); - } -} diff --git a/src/Altinn.App.Core/Interface/IProcessEngine.cs b/src/Altinn.App.Core/Interface/IProcessEngine.cs deleted file mode 100644 index ee2af25a1..000000000 --- a/src/Altinn.App.Core/Interface/IProcessEngine.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Threading.Tasks; -using Altinn.App.Core.Models; - -namespace Altinn.App.Core.Interface -{ - /// - /// Process engine interface that defines the Altinn App process engine - /// - public interface IProcessEngine - { - /// - /// Method to start a new process - /// - Task StartProcess(ProcessChangeContext processChange); - - /// - /// Method to move process to next task/event - /// - Task Next(ProcessChangeContext processChange); - - /// - /// Method to Start Task - /// - Task StartTask(ProcessChangeContext processChange); - } -} diff --git a/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs b/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs index 399cdb9fa..f6d3826f9 100644 --- a/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs +++ b/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs @@ -13,6 +13,7 @@ public class FrontendFeatures : IFrontendFeatures public FrontendFeatures() { features.Add("footer", true); + features.Add("processActions", true); } /// diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs new file mode 100644 index 000000000..15f37be3e --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs @@ -0,0 +1,16 @@ +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties +{ + /// + /// Defines an altinn action for a task + /// + public class AltinnAction + { + /// + /// Gets or sets the ID of the action + /// + [XmlAttribute("id")] + public string Id { get; set; } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnProperties.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnProperties.cs new file mode 100644 index 000000000..55b776202 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnProperties.cs @@ -0,0 +1,24 @@ +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties +{ + /// + /// Defines the altinn properties for a task + /// + public class AltinnProperties + { + /// + /// List of available actions for a task + /// + [XmlArray(ElementName = "actions", Namespace = "http://altinn.no", IsNullable = true)] + [XmlArrayItem(ElementName = "action", Namespace = "http://altinn.no")] + public List? AltinnActions { get; set; } + + /// + /// Gets or sets the task type + /// + //[XmlElement(ElementName = "taskType", Namespace = "http://altinn.no", IsNullable = true)] + [XmlElement("taskType", Namespace = "http://altinn.no")] + public string? TaskType { get; set; } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs new file mode 100644 index 000000000..3efb36de2 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Extended representation of a status object that holds the process state of an application instance. +/// +public class AppProcessElementInfo: ProcessElementInfo +{ + /// + /// Create a new instance of with no fields set. + /// + public AppProcessElementInfo() + { + Actions = new Dictionary(); + } + + /// + /// Create a new instance of with values copied from . + /// + /// The to copy values from. + public AppProcessElementInfo(ProcessElementInfo processElementInfo) + { + Flow = processElementInfo.Flow; + Started = processElementInfo.Started; + ElementId = processElementInfo.ElementId; + Name = processElementInfo.Name; + AltinnTaskType = processElementInfo.AltinnTaskType; + Ended = processElementInfo.Ended; + Validated = processElementInfo.Validated; + FlowType = processElementInfo.FlowType; + Actions = new Dictionary(); + } + /// + /// Actions that can be performed and if the user is allowed to perform them. + /// + [JsonPropertyName(name:"actions")] + public Dictionary? Actions { get; set; } + + /// + /// Indicates if the user has read access to the task. + /// + [JsonPropertyName(name:"read")] + public bool HasReadAccess { get; set; } + + /// + /// Indicates if the user has write access to the task. + /// + [JsonPropertyName(name:"write")] + public bool HasWriteAccess { get; set; } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessState.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessState.cs new file mode 100644 index 000000000..ecfc689c1 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessState.cs @@ -0,0 +1,44 @@ +#nullable enable +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Extended representation of a status object that holds the process state of an application instance. +/// The process is defined by the application's process specification BPMN file. +/// +public class AppProcessState: ProcessState +{ + /// + /// Default constructor + /// + public AppProcessState() + { + } + + /// + /// Constructor that takes a ProcessState object and copies the values. + /// + /// + public AppProcessState(ProcessState? processState) + { + if(processState == null) + { + return; + } + + Started = processState.Started; + StartEvent = processState.StartEvent; + if (processState.CurrentTask != null) + { + CurrentTask = new AppProcessElementInfo(processState.CurrentTask); + } + Ended = processState.Ended; + EndEvent = processState.EndEvent; + } + + /// + /// Gets or sets a status object containing the task info of the currentTask of an ongoing process. + /// + public new AppProcessElementInfo? CurrentTask { get; set; } +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs index fdcdaca36..743d44f6d 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs @@ -1,5 +1,6 @@ using Altinn.App.Core.Interface; using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements { @@ -19,21 +20,21 @@ public ConfirmationTask(ITaskEvents taskEvents) } /// - public override async Task HandleTaskAbandon(ProcessChangeContext processChangeContext) + public override async Task HandleTaskAbandon(string elementId, Instance instance) { - await _taskEvents.OnAbandonProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance); + await _taskEvents.OnAbandonProcessTask(elementId, instance); } /// - public override async Task HandleTaskComplete(ProcessChangeContext processChangeContext) + public override async Task HandleTaskComplete(string elementId, Instance instance) { - await _taskEvents.OnEndProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance); + await _taskEvents.OnEndProcessTask(elementId, instance); } /// - public override async Task HandleTaskStart(ProcessChangeContext processChangeContext) + public override async Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill) { - await _taskEvents.OnStartProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance, processChangeContext.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 index c167e16da..87c93a8e9 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs @@ -1,5 +1,6 @@ using Altinn.App.Core.Interface; using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements { @@ -19,22 +20,22 @@ public DataTask(ITaskEvents taskEvents) } /// - public override async Task HandleTaskAbandon(ProcessChangeContext processChangeContext) + public override async Task HandleTaskAbandon(string elementId, Instance instance) { - await _taskEvents.OnAbandonProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance); + await _taskEvents.OnAbandonProcessTask(elementId, instance); } /// - public override async Task HandleTaskComplete(ProcessChangeContext processChangeContext) + public override async Task HandleTaskComplete(string elementId, Instance instance) { - await _taskEvents.OnEndProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance); + await _taskEvents.OnEndProcessTask(elementId, instance); } /// - public override async Task HandleTaskStart(ProcessChangeContext processChangeContext) + public override async Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill) { - await _taskEvents.OnStartProcessTask(processChangeContext.ElementToBeProcessed, - processChangeContext.Instance, processChangeContext.Prefill); + await _taskEvents.OnStartProcessTask(elementId, + instance, prefill); } } } \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ElementInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/ElementInfo.cs deleted file mode 100644 index 2d0c666a1..000000000 --- a/src/Altinn.App.Core/Internal/Process/Elements/ElementInfo.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Altinn.App.Core.Internal.Process.Elements -{ - /// - /// Represents information about an element in a BPMN description. - /// - public class ElementInfo - { - /// - /// The unique id of a specific element in the BPMN - /// - public string Id { get; set; } - - /// - /// The type of BPMN element - /// - public string ElementType { get; set; } - - /// - /// The name of the BPMN element - /// - public string Name { get; set; } - - /// - /// The altinn specific task type - /// - public string? AltinnTaskType { get; set; } - } -} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs b/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs new file mode 100644 index 000000000..1f624b611 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs @@ -0,0 +1,17 @@ +using System.Xml.Serialization; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +namespace Altinn.App.Core.Internal.Process.Elements +{ + /// + /// Class representing the extension elements + /// + public class ExtensionElements + { + /// + /// Gets or sets the altinn properties + /// + [XmlElement("properties", Namespace = "http://altinn.no")] + public AltinnProperties? AltinnProperties { get; set; } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs index a4f37c312..49ba7ee07 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs @@ -1,5 +1,6 @@ using Altinn.App.Core.Interface; using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements { @@ -19,21 +20,21 @@ public FeedbackTask(ITaskEvents taskEvents) } /// - public override async Task HandleTaskAbandon(ProcessChangeContext processChangeContext) + public override async Task HandleTaskAbandon(string elementId, Instance instance) { - await _taskEvents.OnAbandonProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance); + await _taskEvents.OnAbandonProcessTask(elementId, instance); } /// - public override async Task HandleTaskComplete(ProcessChangeContext processChangeContext) + public override async Task HandleTaskComplete(string elementId, Instance instance) { - await _taskEvents.OnEndProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance); + await _taskEvents.OnEndProcessTask(elementId, instance); } /// - public override async Task HandleTaskStart(ProcessChangeContext processChangeContext) + public override async Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill) { - await _taskEvents.OnStartProcessTask(processChangeContext.ElementToBeProcessed, processChangeContext.Instance, processChangeContext.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 index 813fa1946..ab07f273e 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ITask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ITask.cs @@ -1,4 +1,5 @@ using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements { @@ -10,16 +11,16 @@ 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(ProcessChangeContext processChangeContext); + 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(ProcessChangeContext processChangeContext); + Task HandleTaskComplete(string elementId, Instance instance); /// /// This operatin triggers process logic need to abandon a Task without completing it /// - Task HandleTaskAbandon(ProcessChangeContext processChangeContext); + 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 index f4e151fe5..b8e79271f 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/NullTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/NullTask.cs @@ -1,4 +1,5 @@ using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements; @@ -8,19 +9,19 @@ namespace Altinn.App.Core.Internal.Process.Elements; public class NullTask: ITask { /// - public async Task HandleTaskStart(ProcessChangeContext processChangeContext) + public async Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill) { await Task.CompletedTask; } /// - public async Task HandleTaskComplete(ProcessChangeContext processChangeContext) + public async Task HandleTaskComplete(string elementId, Instance instance) { await Task.CompletedTask; } /// - public async Task HandleTaskAbandon(ProcessChangeContext processChangeContext) + public async Task HandleTaskAbandon(string elementId, Instance instance) { await Task.CompletedTask; } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs index e0b32d758..70b84c207 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs @@ -13,6 +13,12 @@ public class ProcessTask: ProcessElement /// [XmlAttribute("tasktype", Namespace = "http://altinn.no")] public string? TaskType { get; set; } + + /// + /// Defines the extension elements + /// + [XmlElement("extensionElements")] + public ExtensionElements? ExtensionElements { get; set; } /// /// String representation of process element type diff --git a/src/Altinn.App.Core/Internal/Process/Elements/TaskBase.cs b/src/Altinn.App.Core/Internal/Process/Elements/TaskBase.cs index 77f3b528e..274a0a013 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/TaskBase.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/TaskBase.cs @@ -1,4 +1,5 @@ using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements { @@ -10,16 +11,16 @@ public abstract class TaskBase: ITask /// /// hallooo asdf /// - public abstract Task HandleTaskComplete(ProcessChangeContext processChangeContext); + public abstract Task HandleTaskComplete(string elementId, Instance instance); /// /// Handle task start /// - public abstract Task HandleTaskStart(ProcessChangeContext processChangeContext); + public abstract Task HandleTaskStart(string elementId, Instance instance, Dictionary prefill); /// /// Handle task abandon /// - public abstract Task HandleTaskAbandon(ProcessChangeContext processChangeContext); + public abstract Task HandleTaskAbandon(string elementId, Instance instance); } } diff --git a/src/Altinn.App.Core/Internal/Process/IFlowHydration.cs b/src/Altinn.App.Core/Internal/Process/IFlowHydration.cs deleted file mode 100644 index e5218dce1..000000000 --- a/src/Altinn.App.Core/Internal/Process/IFlowHydration.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Altinn.App.Core.Features; -using Altinn.App.Core.Internal.Process.Elements.Base; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Internal.Process; - -/// -/// Defines method needed for filtering process flows based on application configuration -/// -public interface IFlowHydration -{ - /// - /// Checks next elements of current for gateways and apply custom gateway decisions based on implementations - /// - /// Instance data - /// Current process element id - /// Should follow default path out of gateway if set - /// Filtered list of next elements - public Task> NextFollowAndFilterGateways(Instance instance, string? currentElement, bool followDefaults = true); - - /// - /// Takes a list of flows checks for gateways and apply custom gateway decisions based on implementations - /// - /// Instance data - /// Original list of next elements - /// /// Should follow default path out of gateway if set - /// Filtered list of next elements - public Task> NextFollowAndFilterGateways(Instance instance, List originNextElements, bool followDefaults = true); -} diff --git a/src/Altinn.App.Core/Internal/Process/IProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/IProcessEngine.cs new file mode 100644 index 000000000..05ef332e1 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/IProcessEngine.cs @@ -0,0 +1,29 @@ +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process +{ + /// + /// Process engine interface that defines the Altinn App process engine + /// + public interface IProcessEngine + { + /// + /// Method to start a new process + /// + Task StartProcess(ProcessStartRequest processStartRequest); + + /// + /// Method to move process to next task/event + /// + Task Next(ProcessNextRequest request); + + /// + /// Update Instance and rerun instance events + /// + /// + /// + /// + Task UpdateInstanceAndRerunEvents(ProcessStartRequest startRequest, List? events); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/IProcessEventDispatcher.cs b/src/Altinn.App.Core/Internal/Process/IProcessEventDispatcher.cs new file mode 100644 index 000000000..119fedf29 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/IProcessEventDispatcher.cs @@ -0,0 +1,23 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process; + +/// +/// Interface for dispatching events that occur during a process +/// +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); + /// + /// Dispatch events for instance to the events system if AppSettings.RegisterEventsWithEventsComponent is true + /// + /// The instance to dispatch events for + Task RegisterEventWithEventsComponent(Instance instance); +} diff --git a/src/Altinn.App.Core/Internal/Process/IProcessNavigator.cs b/src/Altinn.App.Core/Internal/Process/IProcessNavigator.cs new file mode 100644 index 000000000..75553bdbf --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/IProcessNavigator.cs @@ -0,0 +1,20 @@ +using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process +{ + /// + /// Interface used to descipt the process navigator + /// + public interface IProcessNavigator + { + /// + /// Get the next task in the process from the current element based on the action and datadriven gateway decisions + /// + /// Instance data + /// Current process element id + /// Action performed + /// The next process task + public Task GetNextTask(Instance instance, string currentElement, string? action); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/IProcessReader.cs b/src/Altinn.App.Core/Internal/Process/IProcessReader.cs index 0c5260a76..52c281707 100644 --- a/src/Altinn.App.Core/Internal/Process/IProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/IProcessReader.cs @@ -84,39 +84,26 @@ public interface IProcessReader /// public List GetSequenceFlows(); + /// + /// Get SequenceFlows out of the bpmn element + /// + /// Element to get the outgoing sequenceflows from + /// Outgoing sequence flows + public List GetOutgoingSequenceFlows(ProcessElement? flowElement); /// /// Get ids of all SequenceFlows defined in the process /// /// public List GetSequenceFlowIds(); - + /// /// Find all possible next elements from current element /// /// Current process element id /// public List GetNextElements(string? currentElementId); - - /// - /// Find ids of all possible next elements from current element - /// - /// Current ProcessElement Id - /// - public List GetNextElementIds(string? currentElement); - - /// - /// Get SequenceFlows out of the bpmn element - /// - /// Element to get the outgoing sequenceflows from - /// Outgoing sequence flows - public List GetOutgoingSequenceFlows(ProcessElement? flowElement); - /// - /// Returns a list of sequence flow to be followed between current step and next element - /// - public List GetSequenceFlowsBetween(string? currentStepId, string? nextElementId); - /// /// Returns StartEvent, Task or EndEvent with given Id, null if element not found /// @@ -124,11 +111,4 @@ public interface IProcessReader /// or null public ProcessElement? GetFlowElement(string? elementId); - /// - /// Retuns ElementInfo for StartEvent, Task or EndEvent with given Id, null if element not found - /// - /// Id of element to look for - /// or null - public ElementInfo? GetElementInfo(string? elementId); - } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessChangeHandler.cs b/src/Altinn.App.Core/Internal/Process/ProcessChangeHandler.cs deleted file mode 100644 index ab3442956..000000000 --- a/src/Altinn.App.Core/Internal/Process/ProcessChangeHandler.cs +++ /dev/null @@ -1,449 +0,0 @@ -using System.Security.Claims; -using Altinn.App.Core.Configuration; -using Altinn.App.Core.Extensions; -using Altinn.App.Core.Features.Validation; -using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; -using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.Core.Models; -using Altinn.App.Core.Models.Validation; -using Altinn.Platform.Profile.Models; -using Altinn.Platform.Storage.Interface.Enums; -using Altinn.Platform.Storage.Interface.Models; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Altinn.App.Core.Internal.Process -{ - /// - /// Handler that implements needed logic related to different process changes. Identifies the correct types of tasks and trigger the different task and event - /// - /// While ProcessEngine.cs only understand standard BPMN process this handler fully understand the Altinn App context - /// - public class ProcessChangeHandler : IProcessChangeHandler - { - private readonly IInstance _instanceClient; - private readonly IProcess _processService; - private readonly IProcessReader _processReader; - private readonly ILogger _logger; - private readonly IValidation _validationService; - private readonly IEvents _eventsService; - private readonly IProfile _profileClient; - private readonly AppSettings _appSettings; - private readonly IAppEvents _appEvents; - private readonly ITaskEvents _taskEvents; - - /// - /// Altinn App specific process change handler - /// - public ProcessChangeHandler( - ILogger logger, - IProcess processService, - IProcessReader processReader, - IInstance instanceClient, - IValidation validationService, - IEvents eventsService, - IProfile profileClient, - IOptions appSettings, - IAppEvents appEvents, - ITaskEvents taskEvents) - { - _logger = logger; - _processService = processService; - _instanceClient = instanceClient; - _processReader = processReader; - _validationService = validationService; - _eventsService = eventsService; - _profileClient = profileClient; - _appSettings = appSettings.Value; - _appEvents = appEvents; - _taskEvents = taskEvents; - } - - /// - public async Task HandleMoveToNext(ProcessChangeContext processChange) - { - processChange.ProcessStateChange = await ProcessNext(processChange.Instance, processChange.RequestedProcessElementId, processChange.User); - if (processChange.ProcessStateChange != null) - { - processChange.Instance = await UpdateProcessAndDispatchEvents(processChange); - - await RegisterEventWithEventsComponent(processChange.Instance); - } - - return processChange; - } - - /// - public async Task HandleStart(ProcessChangeContext processChange) - { - // start process - ProcessStateChange startChange = await ProcessStart(processChange.Instance, processChange.ProcessFlowElements[0], processChange.User); - InstanceEvent startEvent = CopyInstanceEventValue(startChange.Events.First()); - - ProcessStateChange nextChange = await ProcessNext(processChange.Instance, processChange.ProcessFlowElements[1], processChange.User); - InstanceEvent goToNextEvent = CopyInstanceEventValue(nextChange.Events.First()); - - ProcessStateChange processStateChange = new ProcessStateChange - { - OldProcessState = startChange.OldProcessState, - NewProcessState = nextChange.NewProcessState, - Events = new List { startEvent, goToNextEvent } - }; - processChange.ProcessStateChange = processStateChange; - - if (!processChange.DontUpdateProcessAndDispatchEvents) - { - processChange.Instance = await UpdateProcessAndDispatchEvents(processChange); - } - - return processChange; - } - - /// - public async Task HandleStartTask(ProcessChangeContext processChange) - { - processChange.Instance = await UpdateProcessAndDispatchEvents(processChange); - return processChange; - } - - /// - public async Task CanTaskBeEnded(ProcessChangeContext processChange) - { - List validationIssues = new List(); - - bool canEndTask; - - if (processChange.Instance.Process?.CurrentTask?.Validated == null || !processChange.Instance.Process.CurrentTask.Validated.CanCompleteTask) - { - validationIssues = await _validationService.ValidateAndUpdateProcess(processChange.Instance, processChange.Instance.Process.CurrentTask?.ElementId); - - canEndTask = await ProcessHelper.CanEndProcessTask(processChange.Instance, validationIssues); - } - else - { - canEndTask = await ProcessHelper.CanEndProcessTask(processChange.Instance, validationIssues); - } - - return canEndTask; - } - - /// - /// 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; - } - - /// - /// This - /// - private async Task UpdateProcessAndDispatchEvents(ProcessChangeContext processChangeContext) - { - await HandleProcessChanges(processChangeContext); - - // 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(processChangeContext.Instance); - await _processService.DispatchProcessEventsToStorage(updatedInstance, processChangeContext.ProcessStateChange.Events); - - // remember to get the instance anew since AppBase can have updated a data element or stored something in the database. - updatedInstance = await _instanceClient.GetInstance(updatedInstance); - - return updatedInstance; - } - - /// - /// Will for each process change trigger relevant Process Elements to perform the relevant change actions. - /// - /// Each implementation - /// - internal async Task HandleProcessChanges(ProcessChangeContext processChangeContext) - { - foreach (InstanceEvent processEvent in processChangeContext.ProcessStateChange.Events) - { - if (Enum.TryParse(processEvent.EventType, true, out InstanceEventType eventType)) - { - processChangeContext.ElementToBeProcessed = processEvent.ProcessInfo?.CurrentTask?.ElementId; - ITask task = GetProcessTask(processEvent.ProcessInfo?.CurrentTask?.AltinnTaskType); - switch (eventType) - { - case InstanceEventType.process_StartEvent: - break; - case InstanceEventType.process_StartTask: - await task.HandleTaskStart(processChangeContext); - break; - case InstanceEventType.process_EndTask: - await task.HandleTaskComplete(processChangeContext); - break; - case InstanceEventType.process_AbandonTask: - await task.HandleTaskAbandon(processChangeContext); - await _instanceClient.UpdateProcess(processChangeContext.Instance); - break; - case InstanceEventType.process_EndEvent: - processChangeContext.ElementToBeProcessed = processEvent.ProcessInfo?.EndEvent; - await _appEvents.OnEndAppEvent(processEvent.ProcessInfo?.EndEvent, processChangeContext.Instance); - break; - } - } - } - } - - /// - /// Does not save process. Instance is updated. - /// - private async Task ProcessStart(Instance instance, string startEvent, ClaimsPrincipal user) - { - if (instance.Process == null) - { - DateTime now = DateTime.UtcNow; - - ProcessState startState = new ProcessState - { - Started = now, - StartEvent = startEvent, - CurrentTask = new ProcessElementInfo { Flow = 1 } - }; - - instance.Process = startState; - - List events = new List - { - await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString(), instance, now, user), - }; - - return new ProcessStateChange - { - OldProcessState = null!, - NewProcessState = startState, - Events = events, - }; - } - - return null; - } - - private async Task GenerateProcessChangeEvent(string eventType, Instance instance, DateTime now, ClaimsPrincipal user) - { - int? userId = user.GetUserIdAsInt(); - InstanceEvent instanceEvent = new InstanceEvent - { - InstanceId = instance.Id, - InstanceOwnerPartyId = instance.InstanceOwner.PartyId, - EventType = eventType, - Created = now, - User = new PlatformUser - { - UserId = userId, - AuthenticationLevel = user.GetAuthenticationLevel(), - OrgId = user.GetOrg() - }, - ProcessInfo = instance.Process, - }; - - if (string.IsNullOrEmpty(instanceEvent.User.OrgId) && userId != null) - { - UserProfile up = await _profileClient.GetUserProfile((int)userId); - instanceEvent.User.NationalIdentityNumber = up.Party.SSN; - } - - return instanceEvent; - } - - private static InstanceEvent CopyInstanceEventValue(InstanceEvent e) - { - return new InstanceEvent - { - Created = e.Created, - DataId = e.DataId, - EventType = e.EventType, - Id = e.Id, - InstanceId = e.InstanceId, - InstanceOwnerPartyId = e.InstanceOwnerPartyId, - ProcessInfo = new ProcessState - { - Started = e.ProcessInfo?.Started, - CurrentTask = new ProcessElementInfo - { - Flow = e.ProcessInfo?.CurrentTask.Flow, - AltinnTaskType = e.ProcessInfo?.CurrentTask.AltinnTaskType, - ElementId = e.ProcessInfo?.CurrentTask.ElementId, - Name = e.ProcessInfo?.CurrentTask.Name, - Started = e.ProcessInfo?.CurrentTask.Started, - Ended = e.ProcessInfo?.CurrentTask.Ended, - Validated = new ValidationStatus - { - CanCompleteTask = e.ProcessInfo?.CurrentTask?.Validated?.CanCompleteTask ?? false, - Timestamp = e.ProcessInfo?.CurrentTask?.Validated?.Timestamp - } - }, - - StartEvent = e.ProcessInfo?.StartEvent - }, - User = new PlatformUser - { - AuthenticationLevel = e.User.AuthenticationLevel, - EndUserSystemId = e.User.EndUserSystemId, - OrgId = e.User.OrgId, - UserId = e.User.UserId, - NationalIdentityNumber = e.User?.NationalIdentityNumber - } - }; - } - - /// - /// Moves instance's process to nextElement id. Returns the instance together with process events. - /// - public async Task ProcessNext(Instance instance, string? nextElementId, ClaimsPrincipal userContext) - { - 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, nextElementId, userContext); - result.NewProcessState = instance.Process; - return result; - } - - return null; - } - - /// - /// Assumes that nextElementId is a valid task/state - /// - private async Task> MoveProcessToNext( - Instance instance, - string? nextElementId, - ClaimsPrincipal user) - { - List events = new List(); - - ProcessState previousState = Copy(instance.Process); - ProcessState currentState = instance.Process; - string? previousElementId = currentState.CurrentTask?.ElementId; - - ElementInfo? nextElementInfo = _processReader.GetElementInfo(nextElementId); - List flows = _processReader.GetSequenceFlowsBetween(previousElementId, nextElementId); - ProcessSequenceFlowType sequenceFlowType = ProcessHelper.GetSequenceFlowType(flows); - DateTime now = DateTime.UtcNow; - bool previousIsProcessTask = _processReader.IsProcessTask(previousElementId); - // ending previous element if task - if (previousIsProcessTask && sequenceFlowType.Equals(ProcessSequenceFlowType.CompleteCurrentMoveToNext)) - { - instance.Process = previousState; - events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_EndTask.ToString(), instance, now, user)); - instance.Process = currentState; - } - else if (previousIsProcessTask) - { - instance.Process = previousState; - events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_AbandonTask.ToString(), instance, now, user)); - instance.Process = currentState; - } - - // ending process if next element is end event - if (_processReader.IsEndEvent(nextElementId)) - { - currentState.CurrentTask = null; - currentState.Ended = now; - currentState.EndEvent = nextElementId; - - events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_EndEvent.ToString(), instance, now, user)); - - // add submit event (to support Altinn2 SBL) - events.Add(await GenerateProcessChangeEvent(InstanceEventType.Submited.ToString(), instance, now, user)); - } - else if (_processReader.IsProcessTask(nextElementId)) - { - currentState.CurrentTask = new ProcessElementInfo - { - Flow = currentState.CurrentTask.Flow + 1, - ElementId = nextElementId, - Name = nextElementInfo?.Name, - Started = now, - AltinnTaskType = nextElementInfo?.AltinnTaskType, - Validated = null, - FlowType = sequenceFlowType.ToString(), - }; - - events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_StartTask.ToString(), instance, now, user)); - } - - // current state points to the instance's process object. The following statement is unnecessary, but clarifies logic. - instance.Process = currentState; - - return events; - } - - private async Task RegisterEventWithEventsComponent(Instance instance) - { - if (_appSettings.RegisterEventsWithEventsComponent) - { - try - { - if (!string.IsNullOrWhiteSpace(instance.Process.CurrentTask?.ElementId)) - { - await _eventsService.AddEvent($"app.instance.process.movedTo.{instance.Process.CurrentTask.ElementId}", instance); - } - else if (instance.Process.EndEvent != null) - { - await _eventsService.AddEvent("app.instance.process.completed", instance); - } - } - catch (Exception exception) - { - _logger.LogWarning(exception, "Exception when sending event with the Events component"); - } - } - } - - private static ProcessState Copy(ProcessState original) - { - ProcessState processState = new ProcessState(); - - if (original.CurrentTask != null) - { - processState.CurrentTask = new ProcessElementInfo(); - processState.CurrentTask.FlowType = original.CurrentTask.FlowType; - processState.CurrentTask.Name = original.CurrentTask.Name; - processState.CurrentTask.Validated = original.CurrentTask.Validated; - processState.CurrentTask.AltinnTaskType = original.CurrentTask.AltinnTaskType; - processState.CurrentTask.Flow = original.CurrentTask.Flow; - processState.CurrentTask.ElementId = original.CurrentTask.ElementId; - processState.CurrentTask.Started = original.CurrentTask.Started; - processState.CurrentTask.Ended = original.CurrentTask.Ended; - } - - processState.EndEvent = original.EndEvent; - processState.Started = original.Started; - processState.Ended = original.Ended; - processState.StartEvent = original.StartEvent; - - return processState; - } - } -} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 2f08f41ca..eb8b7e022 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -1,138 +1,289 @@ +using System.Security.Claims; +using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.Base; using Altinn.App.Core.Models; -using Microsoft.IdentityModel.Tokens; +using Altinn.Platform.Profile.Models; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process +namespace Altinn.App.Core.Internal.Process; + +/// +/// Default implementation of the +/// +public class ProcessEngine : IProcessEngine { + private readonly IProcessReader _processReader; + private readonly IProfile _profileService; + private readonly IProcessNavigator _processNavigator; + private readonly IProcessEventDispatcher _processEventDispatcher; + /// - /// The process engine is responsible for all BMPN related functionality - /// - /// It will call processChange handler that is responsible - /// for the business logic happening for any process change. + /// Initializes a new instance of the class /// - public class ProcessEngine : IProcessEngine + /// + /// + /// + /// + public ProcessEngine( + IProcessReader processReader, + IProfile profileService, + IProcessNavigator processNavigator, + IProcessEventDispatcher processEventDispatcher) { - private readonly IProcessChangeHandler _processChangeHandler; - - private readonly IProcessReader _processReader; - private readonly IFlowHydration _flowHydration; - - /// - /// Initializes a new instance of the class. - /// - public ProcessEngine( - IProcessChangeHandler processChangeHandler, - IProcessReader processReader, - IFlowHydration flowHydration) - { - _processChangeHandler = processChangeHandler; - _processReader = processReader; - _flowHydration = flowHydration; - } + _processReader = processReader; + _profileService = profileService; + _processNavigator = processNavigator; + _processEventDispatcher = processEventDispatcher; + } - /// - /// Move process to next element in process - /// - public async Task Next(ProcessChangeContext processChange) + /// + public async Task StartProcess(ProcessStartRequest processStartRequest) + { + if (processStartRequest.Instance.Process != null) { - string? currentElementId = processChange.Instance.Process.CurrentTask?.ElementId; - - if (currentElementId == null) + return new ProcessChangeResult() { - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = $"Instance does not have current task information!", Type = "Conflict" }); - processChange.FailedProcessChange = true; - return processChange; - } + Success = false, + ErrorMessage = "Process is already started. Use next.", + ErrorType = ProcessErrorType.Conflict + }; + } - if (currentElementId.Equals(processChange.RequestedProcessElementId)) + string? validStartElement = ProcessHelper.GetValidStartEventOrError(processStartRequest.StartEventId, _processReader.GetStartEventIds(), out ProcessError? startEventError); + if (startEventError != null) + { + return new ProcessChangeResult() { - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = $"Requested process element {processChange.RequestedProcessElementId} is same as instance's current task. Cannot change process.", Type = "Conflict" }); - processChange.FailedProcessChange = true; - return processChange; - } + Success = false, + ErrorMessage = "No matching startevent", + ErrorType = ProcessErrorType.Conflict + }; + } - // Find next valid element. Later this will be dynamic - List possibleNextElements = await _flowHydration.NextFollowAndFilterGateways(processChange.Instance, currentElementId, processChange.RequestedProcessElementId.IsNullOrEmpty()); - processChange.RequestedProcessElementId = ProcessHelper.GetValidNextElementOrError(processChange.RequestedProcessElementId, possibleNextElements.Select(e => e.Id).ToList(),out ProcessError? nextElementError); - if (nextElementError != null) + // start process + ProcessStateChange? startChange = await ProcessStart(processStartRequest.Instance, validStartElement!, processStartRequest.User); + InstanceEvent? startEvent = startChange?.Events?.First().CopyValues(); + ProcessStateChange? nextChange = await ProcessNext(processStartRequest.Instance, processStartRequest.User); + InstanceEvent? goToNextEvent = nextChange?.Events?.First().CopyValues(); + List events = new List(); + if (startEvent is not null) + { + events.Add(startEvent); + } + if (goToNextEvent is not null) + { + events.Add(goToNextEvent); + } + ProcessStateChange processStateChange = new ProcessStateChange + { + OldProcessState = startChange?.OldProcessState, + NewProcessState = nextChange?.NewProcessState, + Events = events + }; + + if (!processStartRequest.Dryrun) + { + await _processEventDispatcher.UpdateProcessAndDispatchEvents(processStartRequest.Instance, processStartRequest.Prefill, events); + } + + return new ProcessChangeResult() + { + Success = true, + ProcessStateChange = processStateChange + }; + } + + /// + public async Task Next(ProcessNextRequest request) + { + var instance = request.Instance; + string? currentElementId = instance.Process?.CurrentTask?.ElementId; + + if (currentElementId == null) + { + return new ProcessChangeResult() { - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = nextElementError.Text, Type = "Conflict" }); - processChange.FailedProcessChange = true; - return processChange; - } + Success = false, + ErrorMessage = $"Instance does not have current task information!", + ErrorType = ProcessErrorType.Conflict + }; + } + + var nextResult = await HandleMoveToNext(instance, request.User, request.Action); - List flows = _processReader.GetSequenceFlowsBetween(currentElementId, processChange.RequestedProcessElementId); - processChange.ProcessSequenceFlowType = ProcessHelper.GetSequenceFlowType(flows); + return new ProcessChangeResult() + { + Success = true, + ProcessStateChange = nextResult + }; + } + + /// + public async Task UpdateInstanceAndRerunEvents(ProcessStartRequest startRequest, List? events) + { + return await _processEventDispatcher.UpdateProcessAndDispatchEvents(startRequest.Instance, startRequest.Prefill, events); + } + + /// + /// Does not save process. Instance object is updated. + /// + private async Task ProcessStart(Instance instance, string startEvent, ClaimsPrincipal user) + { + if (instance.Process == null) + { + DateTime now = DateTime.UtcNow; - if (processChange.ProcessSequenceFlowType.Equals(ProcessSequenceFlowType.CompleteCurrentMoveToNext) && await _processChangeHandler.CanTaskBeEnded(processChange)) + ProcessState startState = new ProcessState { - return await _processChangeHandler.HandleMoveToNext(processChange); - } + Started = now, + StartEvent = startEvent, + CurrentTask = new ProcessElementInfo { Flow = 1, ElementId = startEvent} + }; + + instance.Process = startState; - if (processChange.ProcessSequenceFlowType.Equals(ProcessSequenceFlowType.AbandonCurrentReturnToNext)) + List events = new List { - return await _processChangeHandler.HandleMoveToNext(processChange); - } + await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString(), instance, now, user), + }; - processChange.FailedProcessChange = true; - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = $"Cannot complete/close current task {currentElementId}. The data element(s) assigned to the task are not valid!", Type = "conflict" }); - return processChange; + return new ProcessStateChange + { + OldProcessState = null!, + NewProcessState = startState, + Events = events, + }; } - /// - /// Start application process and goes to first valid Task - /// - public async Task StartProcess(ProcessChangeContext processChange) + return null; + } + + /// + /// Moves instance's process to nextElement id. Returns the instance together with process events. + /// + private async Task ProcessNext(Instance instance, ClaimsPrincipal userContext, string? action = null) + { + if (instance.Process != null) { - if (processChange.Instance.Process != null) + ProcessStateChange result = new ProcessStateChange { - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = "Process is already started. Use next.", Type = "Conflict" }); - processChange.FailedProcessChange = true; - return processChange; - } + 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; + } - string? validStartElement = ProcessHelper.GetValidStartEventOrError(processChange.RequestedProcessElementId, _processReader.GetStartEventIds(),out ProcessError? startEventError); - if (startEventError != null) + return null; + } + + private async Task> MoveProcessToNext( + Instance instance, + ClaimsPrincipal user, + string? action = null) + { + List events = new List(); + + ProcessState previousState = instance.Process.Copy(); + ProcessState currentState = instance.Process; + string? previousElementId = currentState.CurrentTask?.ElementId; + + ProcessElement? nextElement = await _processNavigator.GetNextTask(instance, instance.Process.CurrentTask.ElementId, action); + DateTime now = DateTime.UtcNow; + // ending previous element if task + if (_processReader.IsProcessTask(previousElementId)) + { + instance.Process = previousState; + string eventType = InstanceEventType.process_EndTask.ToString(); + if (action is "reject") { - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = "No matching startevent", Type = "Conflict" }); - processChange.FailedProcessChange = true; - return processChange; + eventType = InstanceEventType.process_AbandonTask.ToString(); } + events.Add(await GenerateProcessChangeEvent(eventType, instance, now, user)); + instance.Process = currentState; + } - processChange.ProcessFlowElements = new List(); - processChange.ProcessFlowElements.Add(validStartElement!); + // ending process if next element is end event + if (_processReader.IsEndEvent(nextElement?.Id)) + { + currentState.CurrentTask = null; + currentState.Ended = now; + currentState.EndEvent = nextElement!.Id; + + events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_EndEvent.ToString(), instance, now, user)); - // find next task - List possibleNextElements = (await _flowHydration.NextFollowAndFilterGateways(processChange.Instance, validStartElement)); - string? nextValidElement = ProcessHelper.GetValidNextElementOrError(null, possibleNextElements.Select(e => e.Id).ToList(),out ProcessError? nextElementError); - if (nextElementError != null) + // add submit event (to support Altinn2 SBL) + events.Add(await GenerateProcessChangeEvent(InstanceEventType.Submited.ToString(), instance, now, user)); + } + else if (_processReader.IsProcessTask(nextElement?.Id)) + { + var task = nextElement as ProcessTask; + currentState.CurrentTask = new ProcessElementInfo { - processChange.ProcessMessages = new List(); - processChange.ProcessMessages.Add(new ProcessChangeInfo() { Message = $"Unable to goto next element due to {nextElementError.Code}-{nextElementError.Text}", Type = "Conflict" }); - processChange.FailedProcessChange = true; - return processChange; - } + Flow = currentState.CurrentTask?.Flow + 1, + ElementId = nextElement!.Id, + Name = nextElement!.Name, + Started = now, + AltinnTaskType = task?.ExtensionElements?.AltinnProperties?.TaskType, + Validated = null, + }; + + events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_StartTask.ToString(), instance, now, user)); + } + + // current state points to the instance's process object. The following statement is unnecessary, but clarifies logic. + instance.Process = currentState; + + return events; + } - processChange.ProcessFlowElements.Add(nextValidElement!); + private async Task GenerateProcessChangeEvent(string eventType, Instance instance, DateTime now, ClaimsPrincipal user) + { + int? userId = user.GetUserIdAsInt(); + InstanceEvent instanceEvent = new InstanceEvent + { + InstanceId = instance.Id, + InstanceOwnerPartyId = instance.InstanceOwner.PartyId, + EventType = eventType, + Created = now, + User = new PlatformUser + { + UserId = userId, + AuthenticationLevel = user.GetAuthenticationLevel(), + OrgId = user.GetOrg() + }, + ProcessInfo = instance.Process, + }; - return await _processChangeHandler.HandleStart(processChange); + if (string.IsNullOrEmpty(instanceEvent.User.OrgId) && userId != null) + { + UserProfile up = await _profileService.GetUserProfile((int)userId); + instanceEvent.User.NationalIdentityNumber = up.Party.SSN; } - /// - /// Process Start Current task. The main goal is to trigger the Task related business logic seperate from start process - /// - public async Task StartTask(ProcessChangeContext processChange) + return instanceEvent; + } + + private async Task HandleMoveToNext(Instance instance, ClaimsPrincipal user, string? action) + { + var processStateChange = await ProcessNext(instance, user, action); + if (processStateChange != null) { - return await _processChangeHandler.HandleStartTask(processChange); + instance = await _processEventDispatcher.UpdateProcessAndDispatchEvents(instance, new Dictionary(), processStateChange.Events); + + await _processEventDispatcher.RegisterEventWithEventsComponent(instance); } + + return processStateChange; } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs b/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs new file mode 100644 index 000000000..81d4c0b28 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs @@ -0,0 +1,157 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Interface; +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; + +namespace Altinn.App.Core.Internal.Process; + +/// +/// Default implementation of the process event dispatcher +/// +class ProcessEventDispatcher : IProcessEventDispatcher +{ + private readonly IInstance _instanceService; + private readonly IInstanceEvent _instanceEventClient; + private readonly ITaskEvents _taskEvents; + private readonly IAppEvents _appEvents; + private readonly IEvents _eventsService; + private readonly bool _registerWithEventSystem; + private readonly ILogger _logger; + + public ProcessEventDispatcher( + IInstance instanceService, + IInstanceEvent instanceEventClient, + ITaskEvents taskEvents, + IAppEvents appEvents, + IEvents eventsService, + IOptions appSettings, + ILogger logger) + { + _instanceService = instanceService; + _instanceEventClient = instanceEventClient; + _taskEvents = taskEvents; + _appEvents = appEvents; + _eventsService = eventsService; + _registerWithEventSystem = appSettings.Value.RegisterEventsWithEventsComponent; + _logger = logger; + } + + /// + public async Task UpdateProcessAndDispatchEvents(Instance instance, Dictionary? prefill, 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 _instanceService.UpdateProcess(instance); + await DispatchProcessEventsToStorage(updatedInstance, events); + + // remember to get the instance anew since AppBase can have updated a data element or stored something in the database. + updatedInstance = await _instanceService.GetInstance(updatedInstance); + + return updatedInstance; + } + + /// + public async Task RegisterEventWithEventsComponent(Instance instance) + { + if (_registerWithEventSystem) + { + try + { + if (!string.IsNullOrWhiteSpace(instance.Process.CurrentTask?.ElementId)) + { + await _eventsService.AddEvent($"app.instance.process.movedTo.{instance.Process.CurrentTask.ElementId}", instance); + } + else if (instance.Process.EndEvent != null) + { + await _eventsService.AddEvent("app.instance.process.completed", instance); + } + } + catch (Exception exception) + { + _logger.LogWarning(exception, "Exception when sending event with the Events component"); + } + } + } + + + private async Task DispatchProcessEventsToStorage(Instance instance, List? events) + { + string org = instance.Org; + string app = instance.AppId.Split("/")[1]; + + if (events != null) + { + foreach (InstanceEvent instanceEvent in events) + { + instanceEvent.InstanceId = instance.Id; + await _instanceEventClient.SaveInstanceEvent(instanceEvent, org, app); + } + } + } + + /// + /// 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); + await _instanceService.UpdateProcess(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/ProcessException.cs b/src/Altinn.App.Core/Internal/Process/ProcessException.cs index 189898063..9c90c3fd2 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessException.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessException.cs @@ -5,13 +5,6 @@ namespace Altinn.App.Core.Internal.Process /// public class ProcessException : Exception { - /// - /// Initializes a new instance of the class. - /// - public ProcessException() - { - } - /// /// Initializes a new instance of the class with a specified error message. /// @@ -19,15 +12,5 @@ public ProcessException() public ProcessException(string message) : base(message) { } - - /// - /// Initializes a new instance of the class with a specified error - /// message and a reference to the inner exception that is the cause of this exception. - /// - /// The message that describes the error. - /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified - public ProcessException(string message, Exception inner) : base(message, inner) - { - } } } diff --git a/src/Altinn.App.Core/Internal/Process/FlowHydration.cs b/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs similarity index 59% rename from src/Altinn.App.Core/Internal/Process/FlowHydration.cs rename to src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs index 122c94517..6f82781fe 100644 --- a/src/Altinn.App.Core/Internal/Process/FlowHydration.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs @@ -6,33 +6,44 @@ namespace Altinn.App.Core.Internal.Process; /// -/// Class used to get next elements in the process based on the Process and custom implementations of to make gateway decisions. +/// Default implementation of /// -public class FlowHydration: IFlowHydration +public class ProcessNavigator : IProcessNavigator { private readonly IProcessReader _processReader; private readonly ExclusiveGatewayFactory _gatewayFactory; /// - /// Initialize a new instance of FlowHydration + /// Initialize a new instance of /// - /// IProcessReader implementation used to read the process - /// ExclusiveGatewayFactory used to fetch gateway code to be executed - public FlowHydration(IProcessReader processReader, ExclusiveGatewayFactory gatewayFactory) + /// The process reader + /// Service to fetch wanted gateway filter implementation + public ProcessNavigator(IProcessReader processReader, ExclusiveGatewayFactory gatewayFactory) { _processReader = processReader; _gatewayFactory = gatewayFactory; } - - /// - public async Task> NextFollowAndFilterGateways(Instance instance, string? currentElement, bool followDefaults = true) + + + /// + public async Task GetNextTask(Instance instance, string currentElement, string? action) { List directFlowTargets = _processReader.GetNextElements(currentElement); - return await NextFollowAndFilterGateways(instance, directFlowTargets!, followDefaults); + List filteredNext = await NextFollowAndFilterGateways(instance, directFlowTargets, action); + if (filteredNext.Count == 0) + { + return null; + } + + if (filteredNext.Count == 1) + { + return filteredNext[0]; + } + + throw new ProcessException($"Multiple next elements found from {currentElement}. Please supply action and filters or define a default flow."); } - /// - public async Task> NextFollowAndFilterGateways(Instance instance, List originNextElements, bool followDefaults = true) + private async Task> NextFollowAndFilterGateways(Instance instance, List originNextElements, string? action) { List filteredNext = new List(); foreach (var directFlowTarget in originNextElements) @@ -41,6 +52,7 @@ public async Task> NextFollowAndFilterGateways(Instance ins { continue; } + if (!IsGateway(directFlowTarget)) { filteredNext.Add(directFlowTarget); @@ -57,25 +69,26 @@ public async Task> NextFollowAndFilterGateways(Instance ins } else { - filteredList = await gatewayFilter.FilterAsync(outgoingFlows, instance); + filteredList = await gatewayFilter.FilterAsync(outgoingFlows, instance, action); } var defaultSequenceFlow = filteredList.Find(s => s.Id == gateway.Default); - if (followDefaults && defaultSequenceFlow != null) + if (defaultSequenceFlow != null) { var defaultTarget = _processReader.GetFlowElement(defaultSequenceFlow.TargetRef); - filteredNext.AddRange(await NextFollowAndFilterGateways(instance, new List { defaultTarget })); + filteredNext.AddRange(await NextFollowAndFilterGateways(instance, new List { defaultTarget }, action)); } else { - var filteredTargets= filteredList.Select(e => _processReader.GetFlowElement(e.TargetRef)).ToList(); - filteredNext.AddRange(await NextFollowAndFilterGateways(instance, filteredTargets)); + var filteredTargets = filteredList.Select(e => _processReader.GetFlowElement(e.TargetRef)).ToList(); + filteredNext.AddRange(await NextFollowAndFilterGateways(instance, filteredTargets, action)); } } return filteredNext; } + private static bool IsGateway(ProcessElement processElement) { return processElement is ExclusiveGateway; diff --git a/src/Altinn.App.Core/Internal/Process/ProcessNextRequest.cs b/src/Altinn.App.Core/Internal/Process/ProcessNextRequest.cs new file mode 100644 index 000000000..18ebe02cc --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessNextRequest.cs @@ -0,0 +1,23 @@ +using System.Security.Claims; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process; + +/// +/// Class that defines the request for moving the process to the next task +/// +public class ProcessNextRequest +{ + /// + /// The instance to be moved to the next task + /// + public Instance Instance { get; set; } + /// + /// The user that is performing the action + /// + public ClaimsPrincipal User { get; set; } + /// + /// The action that is performed + /// + public string? Action { get; set; } +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs index 9314102e2..ac2573cbd 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs @@ -103,69 +103,6 @@ public List GetSequenceFlowIds() return GetSequenceFlows().Select(s => s.Id).ToList(); } - /// - public List GetNextElements(string? currentElementId) - { - EnsureArgumentNotNull(currentElementId, nameof(currentElementId)); - List nextElements = new List(); - List allElements = GetAllFlowElements(); - if (!allElements.Exists(e => e.Id == currentElementId)) - { - throw new ProcessException($"Unable to find a element using element id {currentElementId}."); - } - - foreach (SequenceFlow sequenceFlow in GetSequenceFlows().FindAll(s => s.SourceRef == currentElementId)) - { - nextElements.AddRange(allElements.FindAll(e => sequenceFlow.TargetRef == e.Id)); - } - - return nextElements; - } - - /// - public List GetNextElementIds(string? currentElement) - { - return GetNextElements(currentElement).Select(e => e.Id).ToList(); - } - - /// - public List GetOutgoingSequenceFlows(ProcessElement? flowElement) - { - if (flowElement == null) - { - return new List(); - } - - return GetSequenceFlows().FindAll(sf => flowElement.Outgoing.Contains(sf.Id)).ToList(); - } - - /// - public List GetSequenceFlowsBetween(string? currentStepId, string? nextElementId) - { - List flowsToReachTarget = new List(); - foreach (SequenceFlow sequenceFlow in _definitions.Process.SequenceFlow.FindAll(s => s.SourceRef == currentStepId)) - { - if (sequenceFlow.TargetRef.Equals(nextElementId)) - { - flowsToReachTarget.Add(sequenceFlow); - return flowsToReachTarget; - } - - if (_definitions.Process.ExclusiveGateway != null && _definitions.Process.ExclusiveGateway.FirstOrDefault(g => g.Id == sequenceFlow.TargetRef) != null) - { - List subGatewayFlows = GetSequenceFlowsBetween(sequenceFlow.TargetRef, nextElementId); - if (subGatewayFlows.Any()) - { - flowsToReachTarget.Add(sequenceFlow); - flowsToReachTarget.AddRange(subGatewayFlows); - return flowsToReachTarget; - } - } - } - - return flowsToReachTarget; - } - /// public ProcessElement? GetFlowElement(string? elementId) { @@ -191,30 +128,43 @@ public List GetSequenceFlowsBetween(string? currentStepId, string? return _definitions.Process.ExclusiveGateway.Find(e => e.Id == elementId); } - + /// - public ElementInfo? GetElementInfo(string? elementId) + public List GetNextElements(string? currentElementId) { - var e = GetFlowElement(elementId); - if (e == null || e is ExclusiveGateway) + EnsureArgumentNotNull(currentElementId, nameof(currentElementId)); + List nextElements = new List(); + List allElements = GetAllFlowElements(); + if (!allElements.Exists(e => e.Id == currentElementId)) { - return null; + throw new ProcessException($"Unable to find a element using element id {currentElementId}."); } - ElementInfo elementInfo = new ElementInfo() - { - Id = e.Id, - Name = e.Name, - ElementType = e.ElementType() - }; - if (e is ProcessTask task) + foreach (SequenceFlow sequenceFlow in GetSequenceFlows().FindAll(s => s.SourceRef == currentElementId)) { - elementInfo.AltinnTaskType = task.TaskType; + nextElements.AddRange(allElements.FindAll(e => sequenceFlow.TargetRef == e.Id)); } - return elementInfo; + return nextElements; } + /// + public List GetOutgoingSequenceFlows(ProcessElement? flowElement) + { + if (flowElement == null) + { + return new List(); + } + + return GetSequenceFlows().FindAll(sf => flowElement.Outgoing.Contains(sf.Id)).ToList(); + } + + private static void EnsureArgumentNotNull(object? argument, string paramName) + { + if (argument == null) + throw new ArgumentNullException(paramName); + } + private List GetAllFlowElements() { List flowElements = new List(); @@ -224,10 +174,4 @@ private List GetAllFlowElements() flowElements.AddRange(GetEndEvents()); return flowElements; } - - private static void EnsureArgumentNotNull(object? argument, string paramName) - { - if (argument == null) - throw new ArgumentNullException(paramName); - } } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessStartRequest.cs b/src/Altinn.App.Core/Internal/Process/ProcessStartRequest.cs new file mode 100644 index 000000000..3303913fa --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessStartRequest.cs @@ -0,0 +1,31 @@ +using System.Security.Claims; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process; + +/// +/// Class that defines the request for starting a new process +/// +public class ProcessStartRequest +{ + /// + /// The instance to be started + /// + public Instance Instance { get; set; } + /// + /// The user that is starting the process + /// + public ClaimsPrincipal User { get; set; } + /// + /// The prefill data supplied when starting the process + /// + public Dictionary? Prefill { get; set; } + /// + /// 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/ProcessChangeContext.cs b/src/Altinn.App.Core/Models/ProcessChangeContext.cs deleted file mode 100644 index faa693e0d..000000000 --- a/src/Altinn.App.Core/Models/ProcessChangeContext.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Security.Claims; -using Altinn.App.Core.Internal.Process; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Models -{ - /// - /// Data entity that will floow between Process Api, Process Engine, Process Handlers and the TaskImpl/Gateway implt - /// - public class ProcessChangeContext - { - /// - /// Initializes a new instance of the class. - /// - public ProcessChangeContext(Instance instance, ClaimsPrincipal user) - { - Instance = instance; - User = user; - } - - /// - /// The current instance - /// - public Instance Instance { get; set; } - - /// - /// The request process element Id - /// - public string? RequestedProcessElementId { get; set; } - - /// - /// The process flow - /// - public List ProcessFlowElements { get; set; } = new List(); - - /// - /// Information messages - /// - public List ProcessMessages { get; set; } - - /// - /// Did process change fail? - /// - public bool FailedProcessChange { get; set; } - - /// - /// The identity performing the process change - /// - public ClaimsPrincipal User { get; set; } - - /// - /// ProcessStateChange - /// - public ProcessStateChange ProcessStateChange { get; set; } - - /// - /// The current process element to be processed - /// - public string ElementToBeProcessed { get; set; } - - /// - /// Process prefill - /// - public Dictionary Prefill { get; set; } - - /// - /// The ProcessSequenceFlowType - /// - public ProcessSequenceFlowType ProcessSequenceFlowType { get; set; } - - /// - /// Defines if the process handler should not handle events - /// - public bool DontUpdateProcessAndDispatchEvents { get; set; } - } -} diff --git a/src/Altinn.App.Core/Models/ProcessChangeInfo.cs b/src/Altinn.App.Core/Models/ProcessChangeInfo.cs deleted file mode 100644 index 5a2e3b9e1..000000000 --- a/src/Altinn.App.Core/Models/ProcessChangeInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Altinn.App.Core.Models -{ - /// - /// Process change info containing information passed around between process engine componentes - /// - public class ProcessChangeInfo - { - /// - /// Type message - /// - public string Type { get; set; } - - /// - /// The message itself - /// - public string Message { get; set; } - } -} diff --git a/src/Altinn.App.Core/Models/ProcessChangeResult.cs b/src/Altinn.App.Core/Models/ProcessChangeResult.cs new file mode 100644 index 000000000..68aa1736c --- /dev/null +++ b/src/Altinn.App.Core/Models/ProcessChangeResult.cs @@ -0,0 +1,37 @@ +namespace Altinn.App.Core.Models +{ + /// + /// Class representing the result of a process change + /// + public class ProcessChangeResult + { + /// + /// Gets or sets a value indicating whether the process change was successful + /// + public bool Success { get; set; } + /// + /// Gets or sets the error message if the process change was not successful + /// + public string? ErrorMessage { get; set; } + /// + /// Gets or sets the error type if the process change was not successful + /// + public ProcessErrorType? ErrorType { get; set; } + + /// + /// Gets or sets the process state change if the process change was successful + /// + public ProcessStateChange? ProcessStateChange { get; set; } + } + + /// + /// Types of errors that can occur during a process change + /// + public enum ProcessErrorType + { + /// + /// The process change was not allowed due to the current state of the process + /// + Conflict + } +} diff --git a/src/Altinn.App.Core/Models/ProcessStateChange.cs b/src/Altinn.App.Core/Models/ProcessStateChange.cs index f5f59e64f..8232857fc 100644 --- a/src/Altinn.App.Core/Models/ProcessStateChange.cs +++ b/src/Altinn.App.Core/Models/ProcessStateChange.cs @@ -12,16 +12,16 @@ public class ProcessStateChange /// /// Gets or sets the old process state /// - public ProcessState OldProcessState { get; set; } + public ProcessState? OldProcessState { get; set; } /// /// Gets or sets the new process state /// - public ProcessState NewProcessState { get; set; } + public ProcessState? NewProcessState { get; set; } /// /// Gets or sets a list of events to be registered. /// - public List Events { get; set; } + public List? Events { get; set; } } } diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs index 62e43ecbf..e86bea762 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs @@ -16,6 +16,7 @@ using Altinn.App.Core.Internal.App; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; +using IProcessEngine = Altinn.App.Core.Internal.Process.IProcessEngine; namespace Altinn.App.Api.Tests.Controllers; diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs index a26bb125a..ae759f000 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs @@ -6,6 +6,7 @@ using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; @@ -20,6 +21,7 @@ using Moq; using Xunit; +using IProcessEngine = Altinn.App.Core.Internal.Process.IProcessEngine; namespace Altinn.App.Api.Tests.Controllers; @@ -349,9 +351,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())) - .ReturnsAsync((ProcessChangeContext pcc) => { return pcc; }); - _processEngine.Setup(p => p.StartTask(It.IsAny())); + _processEngine.Setup(p => p.StartProcess(It.IsAny())) + .ReturnsAsync(() => { return new ProcessChangeResult(){Success = true}; }); + _processEngine.Setup(p => p.UpdateInstanceAndRerunEvents(It.IsAny(), It.IsAny>())); // Act ActionResult actual = await SUT.CopyInstance(Org, AppName, InstanceOwnerPartyId, instanceGuid); diff --git a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs index d0c40b1cb..4cb2638aa 100644 --- a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs @@ -2,7 +2,9 @@ using Altinn.Platform.Register.Models; using System; using System.Collections.Generic; +using System.Security.Claims; using System.Threading.Tasks; +using Altinn.App.Core.Models; namespace Altinn.App.Api.Tests.Mocks { @@ -15,14 +17,14 @@ public class AuthorizationMock : IAuthorization public Task ValidateSelectedParty(int userId, int partyId) { - bool? isvalid = true; - - if (userId == 1) - { - isvalid = false; - } + bool? isvalid = userId != 1; return Task.FromResult(isvalid); } + + public async Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null) + { + throw new NotImplementedException(); + } } } diff --git a/test/Altinn.App.Core.Tests/Extensions/InstanceEventExtensionsTests.cs b/test/Altinn.App.Core.Tests/Extensions/InstanceEventExtensionsTests.cs new file mode 100644 index 000000000..08640c8d4 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Extensions/InstanceEventExtensionsTests.cs @@ -0,0 +1,215 @@ +using Altinn.App.Core.Extensions; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Extensions; + +public class InstanceEventExtensionsTests +{ + [Fact] + public void CopyValues_returns_copy_of_instance_event() + { + InstanceEvent original = new InstanceEvent() + { + Created = DateTime.Now, + DataId = Guid.NewGuid().ToString(), + EventType = "EventType", + Id = Guid.NewGuid(), + InstanceId = Guid.NewGuid().ToString(), + InstanceOwnerPartyId = "1", + ProcessInfo = new ProcessState + { + Started = DateTime.Now, + CurrentTask = new ProcessElementInfo + { + Flow = 1, + AltinnTaskType = "AltinnTaskType", + ElementId = "ElementId", + Name = "Name", + Started = DateTime.Now, + Ended = DateTime.Now, + Validated = new ValidationStatus + { + CanCompleteTask = true, + Timestamp = DateTime.Now + } + }, + StartEvent = "StartEvent" + }, + User = new PlatformUser + { + AuthenticationLevel = 2, + EndUserSystemId = 1, + OrgId = "OrgId", + UserId = 3, + NationalIdentityNumber = "NationalIdentityNumber" + } + }; + InstanceEvent copy = original.CopyValues(); + copy.Should().NotBeSameAs(original); + copy.ProcessInfo.Should().NotBeSameAs(original.ProcessInfo); + copy.ProcessInfo.CurrentTask.Should().NotBeSameAs(original.ProcessInfo.CurrentTask); + copy.ProcessInfo.CurrentTask.Validated.Should().NotBeSameAs(original.ProcessInfo.CurrentTask.Validated); + copy.User.Should().NotBeSameAs(original.User); + copy.Should().BeEquivalentTo(original); + } + + [Fact] + public void CopyValues_returns_copy_of_instance_event_Validated_null() + { + Guid id = Guid.NewGuid(); + string dataGuid = Guid.NewGuid().ToString(); + string instanceGuid = Guid.NewGuid().ToString(); + DateTime now = DateTime.Now; + InstanceEvent original = new InstanceEvent() + { + Created = now, + DataId = dataGuid, + EventType = "EventType", + Id = id, + InstanceId = instanceGuid, + InstanceOwnerPartyId = "1", + ProcessInfo = new ProcessState + { + Started = now, + CurrentTask = new ProcessElementInfo + { + Flow = 1, + AltinnTaskType = "AltinnTaskType", + ElementId = "ElementId", + Name = "Name", + Started = now, + Ended = now, + Validated = null + }, + StartEvent = "StartEvent" + }, + User = new PlatformUser + { + AuthenticationLevel = 2, + EndUserSystemId = 1, + OrgId = "OrgId", + UserId = 3, + NationalIdentityNumber = "NationalIdentityNumber" + } + }; + InstanceEvent expected = new InstanceEvent() + { + Created = now, + DataId = dataGuid, + EventType = "EventType", + Id = id, + InstanceId = instanceGuid, + InstanceOwnerPartyId = "1", + ProcessInfo = new ProcessState + { + Started = now, + CurrentTask = new ProcessElementInfo + { + Flow = 1, + AltinnTaskType = "AltinnTaskType", + ElementId = "ElementId", + Name = "Name", + Started = now, + Ended = now, + Validated = new() + { + Timestamp = null, + CanCompleteTask = false + } + }, + StartEvent = "StartEvent" + }, + User = new PlatformUser + { + AuthenticationLevel = 2, + EndUserSystemId = 1, + OrgId = "OrgId", + UserId = 3, + NationalIdentityNumber = "NationalIdentityNumber" + } + }; + InstanceEvent copy = original.CopyValues(); + copy.Should().NotBeSameAs(original); + copy.ProcessInfo.Should().NotBeSameAs(original.ProcessInfo); + copy.ProcessInfo.CurrentTask.Should().NotBeSameAs(original.ProcessInfo.CurrentTask); + copy.ProcessInfo.CurrentTask.Validated.Should().NotBeSameAs(original.ProcessInfo.CurrentTask.Validated); + copy.User.Should().NotBeSameAs(original.User); + copy.Should().BeEquivalentTo(expected); + } + + [Fact] + public void CopyValues_returns_copy_of_instance_event_CurrentTask_null() + { + Guid id = Guid.NewGuid(); + string dataGuid = Guid.NewGuid().ToString(); + string instanceGuid = Guid.NewGuid().ToString(); + DateTime now = DateTime.Now; + InstanceEvent original = new InstanceEvent() + { + Created = now, + DataId = dataGuid, + EventType = "EventType", + Id = id, + InstanceId = instanceGuid, + InstanceOwnerPartyId = "1", + ProcessInfo = new ProcessState + { + Started = now, + CurrentTask = null, + StartEvent = "StartEvent" + }, + User = new PlatformUser + { + AuthenticationLevel = 2, + EndUserSystemId = 1, + OrgId = "OrgId", + UserId = 3, + NationalIdentityNumber = "NationalIdentityNumber" + } + }; + InstanceEvent expected = new InstanceEvent() + { + Created = now, + DataId = dataGuid, + EventType = "EventType", + Id = id, + InstanceId = instanceGuid, + InstanceOwnerPartyId = "1", + ProcessInfo = new ProcessState + { + Started = now, + CurrentTask = new ProcessElementInfo + { + Flow = null, + AltinnTaskType = null, + ElementId = null, + Name = null, + Started = null, + Ended = null, + Validated = new() + { + Timestamp = null, + CanCompleteTask = false + } + }, + StartEvent = "StartEvent" + }, + User = new PlatformUser + { + AuthenticationLevel = 2, + EndUserSystemId = 1, + OrgId = "OrgId", + UserId = 3, + NationalIdentityNumber = "NationalIdentityNumber" + } + }; + InstanceEvent copy = original.CopyValues(); + copy.Should().NotBeSameAs(original); + copy.ProcessInfo.Should().NotBeSameAs(original.ProcessInfo); + copy.ProcessInfo.CurrentTask.Should().NotBeSameAs(original.ProcessInfo.CurrentTask); + copy.User.Should().NotBeSameAs(original.User); + copy.Should().BeEquivalentTo(expected); + } +} diff --git a/test/Altinn.App.Core.Tests/Extensions/ProcessStateExtensionTests.cs b/test/Altinn.App.Core.Tests/Extensions/ProcessStateExtensionTests.cs new file mode 100644 index 000000000..0e388beaf --- /dev/null +++ b/test/Altinn.App.Core.Tests/Extensions/ProcessStateExtensionTests.cs @@ -0,0 +1,67 @@ +using Altinn.App.Core.Extensions; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Extensions; + +public class ProcessStateExtensionTests +{ + [Fact] + public void Copy_returns_copy_of_process_state() + { + ProcessState original = new ProcessState(); + ProcessState copy = original.Copy(); + Assert.NotSame(original, copy); + } + + [Fact] + public void Copy_returns_state_with_fields_set() + { + ProcessState original = new ProcessState() + { + Ended = DateTime.Now, + Started = DateTime.Now, + StartEvent = "StartEvent", + EndEvent = "EndEvent", + CurrentTask = new ProcessElementInfo() + { + Ended = DateTime.Now, + Started = DateTime.Now, + ElementId = "ElementId", + AltinnTaskType = "AltinnTaskType", + Flow = 1, + FlowType = "FlowType", + Name = "Name", + Validated = new ValidationStatus() + { + Timestamp = DateTime.Now, + CanCompleteTask = true + }, + } + }; + ProcessState copy = original.Copy(); + Assert.NotSame(original, copy); + Assert.NotSame(original.CurrentTask, copy.CurrentTask); + Assert.Same(original.CurrentTask.Validated, copy.CurrentTask.Validated); + copy.Should().BeEquivalentTo(original); + } + + [Fact] + public void Copy_returns_state_with_current_null_when_original_null() + { + ProcessState original = new ProcessState() + { + Ended = DateTime.Now, + Started = DateTime.Now, + StartEvent = "StartEvent", + EndEvent = "EndEvent", + CurrentTask = null + }; + ProcessState copy = original.Copy(); + Assert.NotSame(original, copy); + Assert.Same(original.CurrentTask, copy.CurrentTask); + copy.Should().BeEquivalentTo(original); + } +} diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs index 6add019e6..d281bcb58 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/EventsSubscriptionClientTests.cs @@ -12,11 +12,19 @@ using Moq; using Moq.Protected; using Xunit; +using Xunit.Abstractions; namespace Altinn.App.PlatformServices.Tests.Infrastructure.Clients { public class EventsSubscriptionClientTests { + private readonly ITestOutputHelper _testOutputHelper; + + public EventsSubscriptionClientTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + [Fact] public async Task AddSubscription_ShouldReturnOk() { diff --git a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs index 9f93c3b00..1a88da473 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs @@ -58,7 +58,8 @@ public async void GetApplicationMetadata_desrializes_file_from_disk() }, Features = new Dictionary() { - { "footer", true } + { "footer", true }, + { "processActions", true } } }; var actual = await appMetadata.GetApplicationMetadata(); @@ -127,7 +128,8 @@ public async void GetApplicationMetadata_eformidling_desrializes_file_from_disk( }, Features = new Dictionary() { - { "footer", true } + { "footer", true }, + { "processActions", true } } }; var actual = await appMetadata.GetApplicationMetadata(); diff --git a/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs b/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs index ec0b8df6e..e97c1e619 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs @@ -11,7 +11,8 @@ public async void GetFeatures_returns_list_of_enabled_features() { Dictionary expected = new Dictionary() { - { "footer", true } + { "footer", true }, + { "processActions", true }, }; IFrontendFeatures frontendFeatures = new FrontendFeatures(); var actual = await frontendFeatures.GetFrontendFeatures(); diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Elements/AppProcessStateTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/Elements/AppProcessStateTests.cs new file mode 100644 index 000000000..693d9b09a --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/Elements/AppProcessStateTests.cs @@ -0,0 +1,132 @@ +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process.Elements; + +public class AppProcessStateTests +{ + [Fact] + public void Constructor_with_ProcessState_copies_values() + { + ProcessState input = new ProcessState() + { + Started = DateTime.Now, + StartEvent = "StartEvent", + Ended = DateTime.Now, + EndEvent = "EndEvent", + CurrentTask = new() + { + Started = DateTime.Now, + Ended = DateTime.Now, + Flow = 2, + Name = "Task_1", + Validated = new() + { + Timestamp = DateTime.Now, + CanCompleteTask = false + }, + ElementId = "Task_1", + FlowType = "FlowType", + AltinnTaskType = "data", + } + }; + AppProcessState expected = new AppProcessState() + { + Started = input.Started, + StartEvent = input.StartEvent, + Ended = input.Ended, + EndEvent = input.EndEvent, + CurrentTask = new() + { + Started = input.CurrentTask.Started, + Ended = input.CurrentTask.Ended, + Flow = input.CurrentTask.Flow, + Name = input.CurrentTask.Name, + Validated = new() + { + Timestamp = input.CurrentTask.Validated.Timestamp, + CanCompleteTask = input.CurrentTask.Validated.CanCompleteTask + }, + ElementId = input.CurrentTask.ElementId, + FlowType = input.CurrentTask.FlowType, + AltinnTaskType = input.CurrentTask.AltinnTaskType, + Actions = new Dictionary(), + HasReadAccess = false, + HasWriteAccess = false + } + }; + AppProcessState actual = new(input); + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void Constructor_with_ProcessState_copies_values_validated_null() + { + ProcessState input = new ProcessState() + { + Started = DateTime.Now, + StartEvent = "StartEvent", + Ended = DateTime.Now, + EndEvent = "EndEvent", + CurrentTask = new() + { + Started = DateTime.Now, + Ended = DateTime.Now, + Flow = 2, + Name = "Task_1", + Validated = null, + ElementId = "Task_1", + FlowType = "FlowType", + AltinnTaskType = "data" + } + }; + AppProcessState expected = new AppProcessState() + { + Started = input.Started, + StartEvent = input.StartEvent, + Ended = input.Ended, + EndEvent = input.EndEvent, + CurrentTask = new() + { + Started = input.CurrentTask.Started, + Ended = input.CurrentTask.Ended, + Flow = input.CurrentTask.Flow, + Name = input.CurrentTask.Name, + Validated = null, + ElementId = input.CurrentTask.ElementId, + FlowType = input.CurrentTask.FlowType, + AltinnTaskType = input.CurrentTask.AltinnTaskType, + Actions = new Dictionary(), + HasReadAccess = false, + HasWriteAccess = false + } + }; + AppProcessState actual = new(input); + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void Constructor_with_ProcessState_copies_values_currenttask_null() + { + ProcessState input = new ProcessState() + { + Started = DateTime.Now, + StartEvent = "StartEvent", + Ended = DateTime.Now, + EndEvent = "EndEvent", + CurrentTask = null + }; + AppProcessState expected = new AppProcessState() + { + Started = input.Started, + StartEvent = input.StartEvent, + Ended = input.Ended, + EndEvent = input.EndEvent, + CurrentTask = null + }; + AppProcessState actual = new(input); + actual.Should().BeEquivalentTo(expected); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/FlowHydrationTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/FlowHydrationTests.cs deleted file mode 100644 index dbb7709da..000000000 --- a/test/Altinn.App.Core.Tests/Internal/Process/FlowHydrationTests.cs +++ /dev/null @@ -1,265 +0,0 @@ -using System.Collections.Generic; -using Altinn.App.Core.Features; -using Altinn.App.Core.Internal.Process; -using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.Core.Internal.Process.Elements.Base; -using Altinn.App.PlatformServices.Tests.Internal.Process.StubGatewayFilters; -using Altinn.App.PlatformServices.Tests.Internal.Process.TestUtils; -using Altinn.Platform.Storage.Interface.Models; -using FluentAssertions; -using Xunit; - -namespace Altinn.App.PlatformServices.Tests.Internal.Process; - -public class FlowHydrationTests -{ - [Fact] - public async void NextFollowAndFilterGateways_returns_next_element_if_no_gateway() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-linear.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "Task1"); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = "Bekreft skjemadata", - TaskType = "confirmation", - Incoming = new List { "Flow2" }, - Outgoing = new List { "Flow3" } - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_returns_empty_list_if_no_outgoing_flows() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-linear.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "EndEvent"); - nextElements.Should().BeEmpty(); - } - - [Fact] - public async void NextFollowAndFilterGateways_returns_default_if_no_filtering_is_implemented_and_default_set() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-default.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "Task1"); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_returns_all_if_no_filtering_is_implemented_and_default_set_but_followDefaults_false() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-default.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "Task1", false); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - }, - new EndEvent() - { - Id = "EndEvent", - Incoming = new List { "Flow5", "Flow4" }, - Name = null!, - Outgoing = new List() - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_returns_all_gateway_target_tasks_if_no_filter_and_default() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "Task1"); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - }, - new ProcessTask() - { - Id = "EndEvent", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow4", "Flow5" }, - Outgoing = new List() - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_returns_all_gateway_target_tasks_if_no_filter_and_default_folowDefaults_false() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "Task1", false); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - }, - new ProcessTask() - { - Id = "EndEvent", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow4", "Flow5" }, - Outgoing = new List() - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_runs_custom_filter_and_returns_result() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-with-join-gateway.bpmn", new List() - { - new DataValuesFilter("Gateway1", "choose") - }); - Instance i = new Instance() - { - DataValues = new Dictionary() - { - { "choose", "Flow3" } - } - }; - - List nextElements = await flowHydrator.NextFollowAndFilterGateways(i, "Task1"); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_does_not_run_filter_with_non_matchin_ids() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-with-join-gateway.bpmn", new List() - { - new DataValuesFilter("Foobar", "choose") - }); - Instance i = new Instance() - { - DataValues = new Dictionary() - { - { "choose", "Flow3" } - } - }; - - List nextElements = await flowHydrator.NextFollowAndFilterGateways(i, "Task1"); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - }, - new ProcessTask() - { - Id = "EndEvent", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow6" }, - Outgoing = new List() - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_follows_downstream_gateways() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-with-join-gateway.bpmn", new List()); - List nextElements = await flowHydrator.NextFollowAndFilterGateways(new Instance(), "Task1"); - nextElements.Should().BeEquivalentTo(new List() - { - new ProcessTask() - { - Id = "Task2", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow3" }, - Outgoing = new List { "Flow5" } - }, - new ProcessTask() - { - Id = "EndEvent", - Name = null!, - TaskType = null!, - Incoming = new List { "Flow6" }, - Outgoing = new List() - } - }); - } - - [Fact] - public async void NextFollowAndFilterGateways_runs_custom_filter_and_returns_empty_list_if_all_filtered_out() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-with-join-gateway.bpmn", new List() - { - new DataValuesFilter("Gateway1", "choose1"), - new DataValuesFilter("Gateway2", "choose2") - }); - Instance i = new Instance() - { - DataValues = new Dictionary() - { - { "choose1", "Flow4" }, - { "choose2", "Foobar" } - } - }; - - List nextElements = await flowHydrator.NextFollowAndFilterGateways(i, "Task1"); - nextElements.Should().BeEmpty(); - } - - [Fact] - public async void NextFollowAndFilterGateways_returns_empty_list_if_element_has_no_next() - { - IFlowHydration flowHydrator = SetupFlowHydration("simple-gateway-with-join-gateway.bpmn", new List()); - Instance i = new Instance(); - - List nextElements = await flowHydrator.NextFollowAndFilterGateways(i, "EndEvent"); - nextElements.Should().BeEmpty(); - } - - private static IFlowHydration SetupFlowHydration(string bpmnfile, IEnumerable gatewayFilters) - { - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - return new FlowHydration(pr, new ExclusiveGatewayFactory(gatewayFilters)); - } -} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index 62d4a6f56..30f4cf6a6 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -1,161 +1,925 @@ -using System.Collections.Generic; -using System.IO; -using Altinn.App.Core.Configuration; -using Altinn.App.Core.Features; -using Altinn.App.Core.Infrastructure.Clients.Storage; +#nullable enable +using System.Security.Claims; +using Altinn.App.Core.Extensions; +using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Models; +using Altinn.Platform.Profile.Models; +using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; +using AltinnCore.Authentication.Constants; +using FluentAssertions; +using Moq; +using Newtonsoft.Json; using Xunit; -namespace Altinn.App.PlatformServices.Tests.Internal.Process -{ - /// - /// Test clas for SimpleInstanceMapper - /// - public class ProcessEngineTest - { - [Fact] - public async void MissingCurrentTask() - { - IProcessReader processReader = GetProcessReader(); +namespace Altinn.App.Core.Tests.Internal.Process; - ProcessEngine processEngine = new ProcessEngine(null!, processReader, GetFlowHydration(processReader)); +public class ProcessEngineTest : IDisposable +{ + private Mock _processReaderMock; + private readonly Mock _profileMock; + private readonly Mock _processNavigatorMock; + private readonly Mock _processEventDispatcherMock; - Instance instance = new Instance - { - Process = new ProcessState() - }; + public ProcessEngineTest() + { + _processReaderMock = new(); + _profileMock = new(); + _processNavigatorMock = new(); + _processEventDispatcherMock = new(); + } - ProcessChangeContext processChangeContext = new ProcessChangeContext(instance, null!); + [Fact] + public async Task StartProcess_returns_unsuccessful_when_process_already_started() + { + IProcessEngine 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); + result.Success.Should().BeFalse(); + result.ErrorMessage.Should().Be("Process is already started. Use next."); + result.ErrorType.Should().Be(ProcessErrorType.Conflict); + } - processChangeContext = await processEngine.Next(processChangeContext); + [Fact] + public async Task StartProcess_returns_unsuccessful_when_no_matching_startevent_found() + { + Mock processReaderMock = new(); + processReaderMock.Setup(r => r.GetStartEventIds()).Returns(new List() { "StartEvent_1" }); + IProcessEngine processEngine = GetProcessEngine(processReaderMock); + Instance instance = new Instance(); + ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, StartEventId = "NotTheStartEventYouAreLookingFor" }; + ProcessChangeResult result = await processEngine.StartProcess(processStartRequest); + _processReaderMock.Verify(r => r.GetStartEventIds(), Times.Once); + result.Success.Should().BeFalse(); + result.ErrorMessage.Should().Be("No matching startevent"); + result.ErrorType.Should().Be(ProcessErrorType.Conflict); + } - Assert.True(processChangeContext.FailedProcessChange); - Assert.Equal("Instance does not have current task information!", processChangeContext.ProcessMessages[0].Message); - } + [Fact] + public async Task StartProcess_starts_process_and_moves_to_first_task_without_event_dispatch_when_dryrun() + { + IProcessEngine processEngine = GetProcessEngine(); + Instance instance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + } + }; + ClaimsPrincipal user = new(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd"), + })); + ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, User = user, Dryrun = true }; + ProcessChangeResult result = await processEngine.StartProcess(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); + _processReaderMock.Verify(r => r.IsProcessTask("Task_1"), Times.Once); + _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "StartEvent_1", null), Times.Once); + result.Success.Should().BeTrue(); + } - [Fact] - public async void RequestingCurrentTask() + [Fact] + public async Task StartProcess_starts_process_and_moves_to_first_task() + { + IProcessEngine processEngine = GetProcessEngine(); + Instance instance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + } + }; + ClaimsPrincipal user = new(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd"), + })); + ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, User = user }; + ProcessChangeResult result = await processEngine.StartProcess(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); + _processReaderMock.Verify(r => r.IsProcessTask("Task_1"), Times.Once); + _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "StartEvent_1", null), Times.Once); + var expectedInstance = new Instance() { - IProcessReader processReader = GetProcessReader(); + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + ElementId = "Task_1", + Flow = 2, + AltinnTaskType = "data", + Name = "Utfylling" + }, + StartEvent = "StartEvent_1" + } + }; + var expectedInstanceEvents = new List() + { + new() + { + EventType = InstanceEventType.process_StartEvent.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2 + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "StartEvent_1", + Flow = 1, + Validated = new() + { + CanCompleteTask = false + } + } + } + }, - ProcessEngine processEngine = new ProcessEngine(null!, processReader, GetFlowHydration(processReader)); + new() + { + EventType = InstanceEventType.process_StartTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2, + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + Name = "Utfylling", + AltinnTaskType = "data", + Flow = 2, + Validated = new() + { + CanCompleteTask = false + } + } + } + } + }; + _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + It.Is(i => CompareInstance(expectedInstance, i)), + null, + It.Is>(l => CompareInstanceEvents(l, expectedInstanceEvents)))); + result.Success.Should().BeTrue(); + } - Instance instance = new Instance + [Fact] + public async Task StartProcess_starts_process_and_moves_to_first_task_with_prefill() + { + IProcessEngine processEngine = GetProcessEngine(); + Instance instance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + } + }; + ClaimsPrincipal user = new(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd"), + })); + var prefill = new Dictionary() { { "test", "test" } }; + ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, User = user, Prefill = prefill }; + ProcessChangeResult result = await processEngine.StartProcess(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); + _processReaderMock.Verify(r => r.IsProcessTask("Task_1"), Times.Once); + _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "StartEvent_1", null), Times.Once); + var expectedInstance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + ElementId = "Task_1", + Flow = 2, + AltinnTaskType = "data", + Name = "Utfylling" + }, + StartEvent = "StartEvent_1" + } + }; + var expectedInstanceEvents = new List() + { + new() { - Process = new ProcessState + EventType = InstanceEventType.process_StartEvent.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2 + }, + ProcessInfo = new() { - CurrentTask = new ProcessElementInfo() { ElementId = "Task_1" } + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "StartEvent_1", + Flow = 1, + Validated = new() + { + CanCompleteTask = false + } + } } - }; + }, - ProcessChangeContext processChangeContext = new ProcessChangeContext(instance, null!) + new() { - RequestedProcessElementId = "Task_1" - }; + EventType = InstanceEventType.process_StartTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2, + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + Name = "Utfylling", + AltinnTaskType = "data", + Flow = 2, + Validated = new() + { + CanCompleteTask = false + } + } + } + } + }; + _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + It.Is(i => CompareInstance(expectedInstance, i)), + prefill, + It.Is>(l => CompareInstanceEvents(l, expectedInstanceEvents)))); + result.Success.Should().BeTrue(); + } - processChangeContext = await processEngine.Next(processChangeContext); + [Fact] + public async Task Next_returns_unsuccessful_when_process_null() + { + IProcessEngine processEngine = GetProcessEngine(); + Instance instance = new Instance() { Process = null }; + ProcessNextRequest processNextRequest = new ProcessNextRequest() { Instance = instance }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); + result.Success.Should().BeFalse(); + result.ErrorMessage.Should().Be("Instance does not have current task information!"); + result.ErrorType.Should().Be(ProcessErrorType.Conflict); + } - Assert.True(processChangeContext.FailedProcessChange); - Assert.Equal("Requested process element Task_1 is same as instance's current task. Cannot change process.", processChangeContext.ProcessMessages[0].Message); - } + [Fact] + public async Task Next_returns_unsuccessful_when_process_currenttask_null() + { + IProcessEngine processEngine = GetProcessEngine(); + Instance instance = new Instance() { Process = new ProcessState() { CurrentTask = null } }; + ProcessNextRequest processNextRequest = new ProcessNextRequest() { Instance = instance }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); + result.Success.Should().BeFalse(); + result.ErrorMessage.Should().Be("Instance does not have current task information!"); + result.ErrorType.Should().Be(ProcessErrorType.Conflict); + } - [Fact] - public async void RequestInvalidTask() + [Fact] + public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() + { + var expectedInstance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + ElementId = "Task_2", + Flow = 3, + AltinnTaskType = "confirmation", + Name = "Bekreft" + }, + StartEvent = "StartEvent_1" + } + }; + IProcessEngine processEngine = GetProcessEngine(null, expectedInstance); + Instance instance = new Instance() + { + InstanceOwner = new() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + AltinnTaskType = "data", + Flow = 2, + Validated = new() + { + CanCompleteTask = true + } + } + } + }; + ProcessState originalProcessState = instance.Process.Copy(); + ClaimsPrincipal user = new(new ClaimsIdentity(new List() { - IProcessReader processReader = GetProcessReader(); + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd"), + })); + ProcessNextRequest processNextRequest = new ProcessNextRequest() { Instance = instance, User = user }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); + _processReaderMock.Verify(r => r.IsProcessTask("Task_1"), Times.Once); + _processReaderMock.Verify(r => r.IsEndEvent("Task_2"), Times.Once); + _processReaderMock.Verify(r => r.IsProcessTask("Task_2"), Times.Once); + _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "Task_1", null), Times.Once); + + var expectedInstanceEvents = new List() + { + new() + { + EventType = InstanceEventType.process_EndTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2 + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + Flow = 2, + AltinnTaskType = "data", + Validated = new() + { + CanCompleteTask = true + } + } + } + }, - ProcessEngine processEngine = new ProcessEngine(null!, processReader, GetFlowHydration(processReader)); + new() + { + EventType = InstanceEventType.process_StartTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2, + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_2", + Name = "Bekreft", + AltinnTaskType = "confirmation", + Flow = 3 + } + } + } + }; + _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + 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() + { + Events = expectedInstanceEvents, + NewProcessState = expectedInstance.Process, + OldProcessState = originalProcessState + }); + } - Instance instance = new Instance + [Fact] + public async Task Next_moves_instance_to_next_task_and_produces_abandon_instanceevent_when_action_reject() + { + var expectedInstance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + ElementId = "Task_2", + Flow = 3, + AltinnTaskType = "confirmation", + Name = "Bekreft" + }, + StartEvent = "StartEvent_1" + } + }; + IProcessEngine processEngine = GetProcessEngine(null, expectedInstance); + Instance instance = new Instance() + { + InstanceOwner = new() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + AltinnTaskType = "data", + Flow = 2, + Validated = new() + { + CanCompleteTask = true + } + } + } + }; + ProcessState originalProcessState = instance.Process.Copy(); + ClaimsPrincipal user = new(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd"), + })); + ProcessNextRequest processNextRequest = new ProcessNextRequest() { Instance = instance, User = user, Action = "reject" }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); + _processReaderMock.Verify(r => r.IsProcessTask("Task_1"), Times.Once); + _processReaderMock.Verify(r => r.IsEndEvent("Task_2"), Times.Once); + _processReaderMock.Verify(r => r.IsProcessTask("Task_2"), Times.Once); + _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "Task_1", "reject"), Times.Once); + + var expectedInstanceEvents = new List() + { + new() { - Process = new ProcessState + EventType = InstanceEventType.process_AbandonTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2 + }, + ProcessInfo = new() { - CurrentTask = new ProcessElementInfo() { ElementId = "Task_1" } + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + Flow = 2, + AltinnTaskType = "data", + Validated = new() + { + CanCompleteTask = true + } + } } - }; + }, - ProcessChangeContext processChangeContext = new ProcessChangeContext(instance, null!) + new() { - RequestedProcessElementId = "Task_10" - }; + EventType = InstanceEventType.process_StartTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2, + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_2", + Name = "Bekreft", + AltinnTaskType = "confirmation", + Flow = 3 + } + } + } + }; + _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + 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() + { + Events = expectedInstanceEvents, + NewProcessState = expectedInstance.Process, + OldProcessState = originalProcessState + }); + } - processChangeContext = await processEngine.Next(processChangeContext); + [Fact] + public async Task Next_moves_instance_to_end_event_and_ends_proces() + { + var expectedInstance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + CurrentTask = null, + StartEvent = "StartEvent_1", + EndEvent = "EndEvent_1" + } + }; + IProcessEngine processEngine = GetProcessEngine(null, expectedInstance); + Instance instance = new Instance() + { + InstanceOwner = new() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_2", + AltinnTaskType = "confirmation", + Flow = 3, + Validated = new() + { + CanCompleteTask = true + } + } + } + }; + ProcessState originalProcessState = instance.Process.Copy(); + ClaimsPrincipal user = new(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + })); + ProcessNextRequest processNextRequest = new ProcessNextRequest() { Instance = instance, User = user }; + ProcessChangeResult result = await processEngine.Next(processNextRequest); + _processReaderMock.Verify(r => r.IsProcessTask("Task_2"), Times.Once); + _processReaderMock.Verify(r => r.IsEndEvent("EndEvent_1"), Times.Once); + _profileMock.Verify(p => p.GetUserProfile(1337), Times.Exactly(3)); + _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "Task_2", null), Times.Once); + + var expectedInstanceEvents = new List() + { + new() + { + EventType = InstanceEventType.process_EndTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + AuthenticationLevel = 2, + NationalIdentityNumber = "22927774937" + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_2", + Flow = 3, + AltinnTaskType = "confirmation", + Validated = new() + { + CanCompleteTask = true + } + } + } + }, - Assert.True(processChangeContext.FailedProcessChange); - Assert.Contains("The proposed next element id 'Task_10' is", processChangeContext.ProcessMessages[0].Message); - } + new() + { + EventType = InstanceEventType.process_EndEvent.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + NationalIdentityNumber = "22927774937", + AuthenticationLevel = 2, + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = null, + EndEvent = "EndEvent_1" + } + }, + new() + { + EventType = InstanceEventType.Submited.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + NationalIdentityNumber = "22927774937", + AuthenticationLevel = 2, + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = null, + EndEvent = "EndEvent_1" + } + } + }; + _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + 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() + { + Events = expectedInstanceEvents, + NewProcessState = expectedInstance.Process, + OldProcessState = originalProcessState + }); + } - [Fact] - public async void StartStartedTask() + [Fact] + public async Task UpdateInstanceAndRerunEvents_sends_instance_and_events_to_eventdispatcher() + { + Instance instance = new Instance() + { + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + StartEvent = "StartEvent_1", + CurrentTask = new ProcessElementInfo() + { + ElementId = "Task_1", + Flow = 3, + AltinnTaskType = "confirmation", + Validated = new() + { + CanCompleteTask = true + } + } + } + }; + Instance updatedInstance = new Instance() + { + Org = "ttd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + Process = new ProcessState() + { + StartEvent = "StartEvent_1", + CurrentTask = new ProcessElementInfo() + { + ElementId = "Task_1", + Flow = 3, + AltinnTaskType = "confirmation", + Validated = new() + { + CanCompleteTask = true + } + } + } + }; + Dictionary prefill = new Dictionary() { - IProcessReader processReader = GetProcessReader(); + { "test", "test" } + }; + List events = new List() + { + new() + { + EventType = InstanceEventType.process_AbandonTask.ToString(), + InstanceOwnerPartyId = "1337", + User = new() + { + UserId = 1337, + OrgId = "tdd", + AuthenticationLevel = 2 + }, + ProcessInfo = new() + { + StartEvent = "StartEvent_1", + CurrentTask = new() + { + ElementId = "Task_1", + Flow = 2, + AltinnTaskType = "data", + Validated = new() + { + CanCompleteTask = true + } + } + } + } + }; + IProcessEngine processEngine = GetProcessEngine(null, updatedInstance); + ProcessStartRequest processStartRequest = new ProcessStartRequest() + { + Instance = instance, + Prefill = prefill, + }; + Instance result = await processEngine.UpdateInstanceAndRerunEvents(processStartRequest, events); + _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( + It.Is(i => CompareInstance(instance, i)), + prefill, + It.Is>(l => CompareInstanceEvents(events, l)))); + result.Should().Be(updatedInstance); + } - ProcessEngine processEngine = new ProcessEngine(null!, processReader, GetFlowHydration(processReader)); + private IProcessEngine GetProcessEngine(Mock? processReaderMock = null, Instance? updatedInstance = null) + { + if (processReaderMock == null) + { + _processReaderMock = new(); + _processReaderMock.Setup(r => r.GetStartEventIds()).Returns(new List() { "StartEvent_1" }); + _processReaderMock.Setup(r => r.IsProcessTask("StartEvent_1")).Returns(false); + _processReaderMock.Setup(r => r.IsEndEvent("Task_1")).Returns(false); + _processReaderMock.Setup(r => r.IsProcessTask("Task_1")).Returns(true); + _processReaderMock.Setup(r => r.IsProcessTask("Task_2")).Returns(true); + _processReaderMock.Setup(r => r.IsProcessTask("EndEvent_1")).Returns(false); + _processReaderMock.Setup(r => r.IsEndEvent("EndEvent_1")).Returns(true); + _processReaderMock.Setup(r => r.IsProcessTask("EndEvent_1")).Returns(false); + } + else + { + _processReaderMock = processReaderMock; + } - Instance instance = new Instance + _profileMock.Setup(p => p.GetUserProfile(1337)).ReturnsAsync(() => new UserProfile() + { + UserId = 1337, + Email = "test@example.com", + Party = new Party() { - Process = new ProcessState + SSN = "22927774937" + } + }); + _processNavigatorMock.Setup( + pn => pn.GetNextTask(It.IsAny(), "StartEvent_1", It.IsAny())) + .ReturnsAsync(() => new ProcessTask() + { + Id = "Task_1", + Incoming = new List { "Flow_1" }, + Outgoing = new List { "Flow_2" }, + Name = "Utfylling", + ExtensionElements = new() { - CurrentTask = new ProcessElementInfo() { ElementId = "Task_1" } + AltinnProperties = new() + { + TaskType = "data" + } } - }; - - ProcessChangeContext processChangeContext = new ProcessChangeContext(instance, null!) + }); + _processNavigatorMock.Setup( + pn => pn.GetNextTask(It.IsAny(), "Task_1", It.IsAny())) + .ReturnsAsync(() => new ProcessTask() + { + Id = "Task_2", + Incoming = new List { "Flow_2" }, + Outgoing = new List { "Flow_3" }, + Name = "Bekreft", + ExtensionElements = new() + { + AltinnProperties = new() + { + TaskType = "confirmation" + } + } + }); + _processNavigatorMock.Setup( + pn => pn.GetNextTask(It.IsAny(), "Task_2", It.IsAny())) + .ReturnsAsync(() => new EndEvent() { - RequestedProcessElementId = "Task_10" - }; + Id = "EndEvent_1", + Incoming = new List { "Flow_3" } + }); + if (updatedInstance is not null) + { + _processEventDispatcherMock.Setup(d => d.UpdateProcessAndDispatchEvents(It.IsAny(), It.IsAny?>(), It.IsAny>())) + .ReturnsAsync(() => updatedInstance); + } - processChangeContext = await processEngine.StartProcess(processChangeContext); + return new ProcessEngine( + _processReaderMock.Object, + _profileMock.Object, + _processNavigatorMock.Object, + _processEventDispatcherMock.Object); + } - Assert.True(processChangeContext.FailedProcessChange); - Assert.Contains("Process is already started. Use next.", processChangeContext.ProcessMessages[0].Message); - } + public void Dispose() + { + _processReaderMock.VerifyNoOtherCalls(); + _profileMock.VerifyNoOtherCalls(); + _processNavigatorMock.VerifyNoOtherCalls(); + _processEventDispatcherMock.VerifyNoOtherCalls(); + } - [Fact] - public async void InvalidStartEvent() + private static bool CompareInstance(Instance expected, Instance actual) + { + expected.Process.Started = actual.Process.Started; + expected.Process.Ended = actual.Process.Ended; + if (actual.Process.CurrentTask != null) { - IProcessReader processReader = GetProcessReader(); + expected.Process.CurrentTask.Started = actual.Process.CurrentTask.Started; + } - ProcessEngine processEngine = new ProcessEngine(null!, processReader, GetFlowHydration(processReader)); + return JsonCompare(expected, actual); + } - Instance instance = new Instance(); - - ProcessChangeContext processChangeContext = new ProcessChangeContext(instance, null!) + private static bool CompareInstanceEvents(List expected, List actual) + { + for (int i = 0; i < expected.Count; i++) + { + expected[i].Created = actual[i].Created; + expected[i].ProcessInfo.Started = actual[i].ProcessInfo.Started; + expected[i].ProcessInfo.Ended = actual[i].ProcessInfo.Ended; + if (actual[i].ProcessInfo.CurrentTask != null) { - RequestedProcessElementId = "Task_10" - }; + expected[i].ProcessInfo.CurrentTask.Started = actual[i].ProcessInfo.CurrentTask.Started; + } + } - processChangeContext = await processEngine.StartProcess(processChangeContext); + return JsonCompare(expected, actual); + } - Assert.True(processChangeContext.FailedProcessChange); - Assert.Contains("No matching startevent", processChangeContext.ProcessMessages[0].Message); + public static bool JsonCompare(object expected, object actual) + { + if (ReferenceEquals(expected, actual)) + { + return true; } - private static IProcessReader GetProcessReader() + if ((expected == null) || (actual == null)) { - AppSettings appSettings = new AppSettings - { - AppBasePath = Path.Join("Internal", "Process", "TestData", "ProcessEngineTest") + Path.DirectorySeparatorChar - }; - IOptions appSettingsO = Microsoft.Extensions.Options.Options.Create(appSettings); - - PlatformSettings platformSettings = new PlatformSettings - { - ApiStorageEndpoint = "http://localhost/" - }; - IOptions platformSettings0 = Microsoft.Extensions.Options.Options.Create(platformSettings); - - ProcessClient processClient = new ProcessClient(platformSettings0, appSettingsO, null!, new NullLogger(), null!, new System.Net.Http.HttpClient()); - return new ProcessReader(processClient); + return false; } - private static IFlowHydration GetFlowHydration(IProcessReader processReader) + if (expected.GetType() != actual.GetType()) { - return new FlowHydration(processReader, new ExclusiveGatewayFactory(new List())); + return false; } + + var expectedJson = JsonConvert.SerializeObject(expected); + var actualJson = JsonConvert.SerializeObject(actual); + + return expectedJson == actualJson; } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs new file mode 100644 index 000000000..db1856666 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs @@ -0,0 +1,823 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Process; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process; + +public class ProcessEventDispatcherTests +{ + [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( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_1", + Flow = 2 + } + } + }; + List events = new List() + { + new InstanceEvent() + { + EventType = InstanceEventType.process_StartEvent.ToString(), + ProcessInfo = new() + { + CurrentTask = new() + { + ElementId = "StartEvent", + AltinnTaskType = "start", + Name = "Start" + } + } + } + }; + 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); + + // Assert + result.Should().Be(getInstanceResponse); + 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(); + } + + [Fact] + public async Task UpdateProcessAndDispatchEvents_StartTask_instance_updated_and_events_sent_to_storage_nothing_sent_to_ITask_when_tasktype_missing() + { + // 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( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_1", + Flow = 2 + } + } + }; + List events = new List() + { + new InstanceEvent() + { + EventType = InstanceEventType.process_StartTask.ToString(), + ProcessInfo = new() + { + CurrentTask = new() + { + ElementId = "StartEvent", + Name = "Start" + } + } + } + }; + 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); + + // Assert + result.Should().Be(getInstanceResponse); + 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(); + } + + [Fact] + public async Task UpdateProcessAndDispatchEvents_StartTask_data_instance_updated_and_events_sent_to_storage_and_trigger_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( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_1", + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_1", + Flow = 2 + } + } + }; + List events = new List() + { + new InstanceEvent() + { + EventType = InstanceEventType.process_StartTask.ToString(), + ProcessInfo = new() + { + CurrentTask = new() + { + ElementId = "Task_1", + AltinnTaskType = "data", + Name = "Utfylling", + Flow = 2 + } + } + } + }; + 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); + + // 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(); + } + + [Fact] + public async Task UpdateProcessAndDispatchEvents_EndTask_confirmation_instance_updated_and_events_sent_to_storage_and_trigger_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( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + Flow = 3 + } + } + }; + List events = new List() + { + new InstanceEvent() + { + EventType = InstanceEventType.process_EndTask.ToString(), + ProcessInfo = new() + { + CurrentTask = new() + { + ElementId = "Task_2", + AltinnTaskType = "confirmation", + Name = "Bekreft", + Flow = 2 + } + } + } + }; + 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); + + // 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(); + } + + [Fact] + public async Task UpdateProcessAndDispatchEvents_AbandonTask_feedback_instance_updated_and_events_sent_to_storage_and_trigger_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( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + Flow = 4 + } + } + }; + List events = new List() + { + new InstanceEvent() + { + EventType = InstanceEventType.process_AbandonTask.ToString(), + ProcessInfo = new() + { + CurrentTask = new() + { + ElementId = "Task_2", + AltinnTaskType = "feedback", + Name = "Bekreft", + Flow = 4 + } + } + } + }; + 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); + + // Assert + result.Should().Be(getInstanceResponse); + taskEvents.Verify(t => t.OnAbandonProcessTask("Task_2", instance), Times.Once); + instanceService.Verify(i => i.UpdateProcess(instance), Times.Exactly(2)); + 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(); + } + + [Fact] + public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_updated_and_events_sent_to_storage_and_trigger_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( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + Flow = 3 + } + } + }; + List events = new List() + { + new InstanceEvent() + { + EventType = InstanceEventType.process_EndEvent.ToString(), + ProcessInfo = new() + { + CurrentTask = new() + { + ElementId = "Task_2", + AltinnTaskType = "confirmation", + Name = "Bekreft", + Flow = 2 + }, + EndEvent = "EndEvent" + } + } + }; + 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); + + // Assert + result.Should().Be(getInstanceResponse); + appEvents.Verify(a => a.OnEndAppEvent("EndEvent", 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(); + } + + [Fact] + public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_updated_and_dispatches_no_events_when_events_null() + { + // 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( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Org = "ttd", + AppId = "ttd/test-app", + }; + Instance updateInstanceResponse = new Instance() + { + Id = instance.Id, + Org = "ttd", + AppId = "ttd/test-app", + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + } + } + }; + Instance getInstanceResponse = new Instance() + { + Id = instance.Id, + Process = new ProcessState() + { + CurrentTask = new() + { + ElementId = "Task_2", + Flow = 3 + } + } + }; + List events = null; + 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); + + // Assert + result.Should().Be(getInstanceResponse); + instanceService.Verify(i => i.UpdateProcess(instance), Times.Once); + instanceService.Verify(i => i.GetInstance(updateInstanceResponse), Times.Once); + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task RegisterEventWithEventsComponent_sends_movedTo_event_to_events_system_when_enabled_and_current_task_set() + { + // 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() + { + RegisterEventsWithEventsComponent = true + }); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }; + + // Act + await dispatcher.RegisterEventWithEventsComponent(instance); + + // Assert + eventsService.Verify(e => e.AddEvent("app.instance.process.movedTo.Task_1", instance), Times.Once); + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task RegisterEventWithEventsComponent_sends_complete_event_to_events_system_when_currentTask_null_and_endevent_set() + { + // 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() + { + RegisterEventsWithEventsComponent = true + }); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Process = new() + { + CurrentTask = null, + EndEvent = "EndEvent" + } + }; + + // Act + await dispatcher.RegisterEventWithEventsComponent(instance); + + // Assert + eventsService.Verify(e => e.AddEvent("app.instance.process.completed", instance), Times.Once); + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task RegisterEventWithEventsComponent_sends_no_events_when_process_is_null() + { + // 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() + { + RegisterEventsWithEventsComponent = true + }); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Process = null + }; + + // Act + await dispatcher.RegisterEventWithEventsComponent(instance); + + // Assert + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task RegisterEventWithEventsComponent_sends_no_events_when_current_and_endevent_is_null() + { + // 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() + { + RegisterEventsWithEventsComponent = true + }); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Process = new() + }; + + // Act + await dispatcher.RegisterEventWithEventsComponent(instance); + + // Assert + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } + + [Fact] + public async Task RegisterEventWithEventsComponent_sends_no_events_when_registereventswitheventscomponent_false() + { + // 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() + { + RegisterEventsWithEventsComponent = false + }); + var logger = new NullLogger(); + IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( + instanceService.Object, + instanceEvent.Object, + taskEvents.Object, + appEvents.Object, + eventsService.Object, + appSettings, + logger); + Instance instance = new Instance() + { + Id = Guid.NewGuid().ToString(), + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }; + + // Act + await dispatcher.RegisterEventWithEventsComponent(instance); + + // Assert + instanceService.VerifyNoOtherCalls(); + instanceEvent.VerifyNoOtherCalls(); + taskEvents.VerifyNoOtherCalls(); + appEvents.VerifyNoOtherCalls(); + eventsService.VerifyNoOtherCalls(); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs new file mode 100644 index 000000000..22fdc9519 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs @@ -0,0 +1,190 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Tests.Internal.Process.TestUtils; +using Altinn.App.PlatformServices.Tests.Internal.Process.StubGatewayFilters; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process; + +public class ProcessNavigatorTests +{ + [Fact] + public async void GetNextTask_returns_next_element_if_no_gateway() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-linear.bpmn", new List()); + ProcessElement nextElements = await processNavigator.GetNextTask(new Instance(), "Task1", null); + nextElements.Should().BeEquivalentTo(new ProcessTask() + { + Id = "Task2", + Name = "Bekreft skjemadata", + TaskType = "confirmation", + Incoming = new List { "Flow2" }, + Outgoing = new List { "Flow3" } + }); + } + + [Fact] + public async void NextFollowAndFilterGateways_returns_empty_list_if_no_outgoing_flows() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-linear.bpmn", new List()); + ProcessElement nextElements = await processNavigator.GetNextTask(new Instance(), "EndEvent", null); + nextElements.Should().BeNull(); + } + + [Fact] + public async void GetNextTask_returns_default_if_no_filtering_is_implemented_and_default_set() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-gateway-default.bpmn", new List()); + ProcessElement nextElements = await processNavigator.GetNextTask(new Instance(), "Task1", null); + nextElements.Should().BeEquivalentTo(new ProcessTask() + { + Id = "Task2", + Name = null!, + TaskType = null!, + ExtensionElements = new() + { + AltinnProperties = new() + { + TaskType = "confirm", + AltinnActions = new() + { + new() + { + Id = "confirm" + }, + new() + { + Id = "reject" + } + } + } + }, + Incoming = new List { "Flow3" }, + Outgoing = new List { "Flow5" } + }); + } + + [Fact] + public async void GetNextTask_runs_custom_filter_and_returns_result() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-gateway-with-join-gateway.bpmn", new List() + { + new DataValuesFilter("Gateway1", "choose") + }); + Instance i = new Instance() + { + DataValues = new Dictionary() + { + { "choose", "Flow3" } + } + }; + + ProcessElement nextElements = await processNavigator.GetNextTask(i, "Task1", null); + nextElements.Should().BeEquivalentTo(new ProcessTask() + { + Id = "Task2", + Name = null!, + TaskType = null!, + ExtensionElements = new() + { + AltinnProperties = new() + { + TaskType = "data", + AltinnActions = new() + { + new() + { + Id = "submit" + } + } + } + }, + Incoming = new List { "Flow3" }, + Outgoing = new List { "Flow5" } + }); + } + + [Fact] + public async void GetNextTask_throws_ProcessException_if_multiple_targets_found() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-gateway-with-join-gateway.bpmn", new List() + { + new DataValuesFilter("Foobar", "choose") + }); + Instance i = new Instance() + { + DataValues = new Dictionary() + { + { "choose", "Flow3" } + } + }; + + var result = await Assert.ThrowsAsync(async () => await processNavigator.GetNextTask(i, "Task1", null)); + result.Message.Should().Be("Multiple next elements found from Task1. Please supply action and filters or define a default flow."); + } + + [Fact] + public async void GetNextTask_follows_downstream_gateways() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-gateway-with-join-gateway.bpmn", new List() + { + new DataValuesFilter("Gateway1", "choose1") + }); + Instance i = new Instance() + { + DataValues = new Dictionary() + { + { "choose1", "Flow4" } + } + }; + ProcessElement nextElements = await processNavigator.GetNextTask(i, "Task1", null); + nextElements.Should().BeEquivalentTo(new EndEvent() + { + Id = "EndEvent", + Name = null!, + Incoming = new List { "Flow6" }, + Outgoing = new List() + }); + } + + [Fact] + public async void GetNextTask_runs_custom_filter_and_returns_empty_list_if_all_filtered_out() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-gateway-with-join-gateway.bpmn", new List() + { + new DataValuesFilter("Gateway1", "choose1"), + new DataValuesFilter("Gateway2", "choose2") + }); + Instance i = new Instance() + { + DataValues = new Dictionary() + { + { "choose1", "Flow4" }, + { "choose2", "Bar" } + } + }; + + ProcessElement nextElements = await processNavigator.GetNextTask(i, "Task1", null); + nextElements.Should().BeNull(); + } + + [Fact] + public async void GetNextTask_returns_empty_list_if_element_has_no_next() + { + IProcessNavigator processNavigator = SetupProcessNavigator("simple-gateway-with-join-gateway.bpmn", new List()); + Instance i = new Instance(); + + ProcessElement nextElements = await processNavigator.GetNextTask(i, "EndEvent", null); + nextElements.Should().BeNull(); + } + + private static IProcessNavigator SetupProcessNavigator(string bpmnfile, IEnumerable gatewayFilters) + { + ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); + return new ProcessNavigator(pr, new ExclusiveGatewayFactory(gatewayFilters)); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs index befdcb789..e49a3d3ca 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs @@ -1,14 +1,13 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.PlatformServices.Tests.Internal.Process.TestUtils; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; +using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Tests.Internal.Process.TestUtils; using FluentAssertions; using Xunit; -namespace Altinn.App.PlatformServices.Tests.Internal.Process; +namespace Altinn.App.Core.Tests.Internal.Process; public class ProcessReaderTests { @@ -40,7 +39,7 @@ public void IsStartEvent_returns_false_when_element_is_not_StartEvent() pr.IsStartEvent("Foobar").Should().BeFalse(); pr.IsStartEvent(null).Should().BeFalse(); } - + [Fact] public void IsProcessTask_returns_true_when_element_is_ProcessTask() { @@ -58,7 +57,7 @@ public void IsProcessTask_returns_false_when_element_is_not_ProcessTask() pr.IsProcessTask("Foobar").Should().BeFalse(); pr.IsProcessTask(null).Should().BeFalse(); } - + [Fact] public void IsEndEvent_returns_true_when_element_is_EndEvent() { @@ -76,14 +75,14 @@ public void IsEndEvent_returns_false_when_element_is_not_EndEvent() pr.IsEndEvent("Foobar").Should().BeFalse(); pr.IsEndEvent(null).Should().BeFalse(); } - + [Fact] public void GetNextElement_returns_gateway() { var currentElement = "Task1"; ProcessReader pr = ProcessTestUtils.SetupProcessReader("simple-gateway.bpmn"); - List nextElements = pr.GetNextElementIds(currentElement); - nextElements.Should().Equal("Gateway1"); + List nextElements = pr.GetNextElements(currentElement); + nextElements.Should().BeEquivalentTo(new List() { new ExclusiveGateway() { Id = "Gateway1", Incoming = new List() { "Flow2" }, Outgoing = new List() { "Flow3", "Flow4" } } }); } [Fact] @@ -92,8 +91,19 @@ public void GetNextElement_returns_task() var bpmnfile = "simple-linear.bpmn"; var currentElement = "Task1"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - List nextElements = pr.GetNextElementIds(currentElement); - nextElements.Should().Equal("Task2"); + List nextElements = pr.GetNextElements(currentElement); + nextElements.Should().BeEquivalentTo( + new List + { + new ProcessTask() + { + Id = "Task2", + Incoming = new List { "Flow2" }, + Outgoing = new List { "Flow3" }, + Name = "Bekreft skjemadata", + TaskType = "confirmation" + } + }); } [Fact] @@ -102,131 +112,109 @@ public void GetNextElement_returns_all_targets_after_gateway() var bpmnfile = "simple-gateway.bpmn"; var currentElement = "Gateway1"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - List nextElements = pr.GetNextElementIds(currentElement); - nextElements.Should().Equal("Task2", "EndEvent"); + List nextElements = pr.GetNextElements(currentElement); + nextElements.Should().BeEquivalentTo( + new List + { + new ProcessTask() + { + Id = "Task2", + Incoming = new List() { "Flow3" }, + Outgoing = new List() { "Flow5" }, + }, + new EndEvent() + { + Id = "EndEvent", + Incoming = new List() { "Flow4", "Flow5" }, + Outgoing = new List() + } + }); } - + [Fact] public void GetNextElement_returns_task1_in_simple_process() { var bpmnfile = "simple-linear.bpmn"; var currentElement = "StartEvent"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - List nextElements = pr.GetNextElementIds(currentElement); - nextElements.Should().Equal("Task1"); + List nextElements = pr.GetNextElements(currentElement); + nextElements.Should().BeEquivalentTo( + new List() + { + new ProcessTask() + { + Id = "Task1", + Name = "Utfylling", + Incoming = new List() { "Flow1" }, + Outgoing = new List() { "Flow2" }, + } + }); } - + [Fact] public void GetNextElement_returns_task2_in_simple_process() { var bpmnfile = "simple-linear.bpmn"; var currentElement = "Task1"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - List nextElements = pr.GetNextElementIds(currentElement); - nextElements.Should().Equal("Task2"); + List nextElements = pr.GetNextElements(currentElement); + nextElements.Should().BeEquivalentTo( + new List() + { + new ProcessTask() + { + Id = "Task2", + Name = "Bekreft skjemadata", + Incoming = new List() { "Flow2" }, + Outgoing = new List() { "Flow3" }, + } + }); } - + [Fact] public void GetNextElement_returns_endevent_in_simple_process() { var bpmnfile = "simple-linear.bpmn"; var currentElement = "Task2"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - List nextElements = pr.GetNextElementIds(currentElement); - nextElements.Should().Equal("EndEvent"); + List nextElements = pr.GetNextElements(currentElement); + nextElements.Should().BeEquivalentTo( + new List() + { + new EndEvent() + { + Id = "EndEvent", + Incoming = new List() { "Flow3" }, + Outgoing = new List() + } + }); } - + [Fact] public void GetNextElement_returns_emptylist_if_task_without_output() { var bpmnfile = "simple-no-end.bpmn"; var currentElement = "Task2"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - List nextElements = pr.GetNextElementIds(currentElement); + List nextElements = pr.GetNextElements(currentElement); nextElements.Should().HaveCount(0); } - + [Fact] public void GetNextElement_currentElement_null() { var bpmnfile = "simple-linear.bpmn"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - pr.Invoking(p => p.GetNextElementIds(null!)).Should().Throw(); + pr.Invoking(p => p.GetNextElements(null!)).Should().Throw(); } - + [Fact] public void GetNextElement_throws_exception_if_step_not_found() { var bpmnfile = "simple-linear.bpmn"; var currentElement = "NoStep"; ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - pr.Invoking(p => p.GetNextElementIds(currentElement)).Should().Throw(); - } - - [Fact] - public void GetElementInfo_returns_correct_info_for_ProcessTask() - { - var bpmnfile = "simple-linear.bpmn"; - var currentElement = "Task1"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetElementInfo(currentElement); - actual.Should().BeEquivalentTo(new ElementInfo() - { - Id = "Task1", - Name = "Utfylling", - AltinnTaskType = "data", - ElementType = "Task" - }); - } - - [Fact] - public void GetElementInfo_returns_correct_info_for_StartEvent() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "StartEvent"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetElementInfo(currentElement); - actual.Should().BeEquivalentTo(new ElementInfo() - { - Id = "StartEvent", - Name = null!, - AltinnTaskType = null!, - ElementType = "StartEvent" - }); - } - - [Fact] - public void GetElementInfo_returns_correct_info_for_EndEvent() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "EndEvent"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetElementInfo(currentElement); - actual.Should().BeEquivalentTo(new ElementInfo() - { - Id = "EndEvent", - Name = null!, - AltinnTaskType = null!, - ElementType = "EndEvent" - }); - } - - [Fact] - public void GetElementInfo_returns_null_for_ExclusiveGateway() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "Gateway1"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetElementInfo(currentElement); - actual.Should().BeNull(); - } - - [Fact] - public void GetElementInfo_throws_argument_null_expcetion_when_elementName_is_null() - { - var bpmnfile = "simple-linear.bpmn"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - pr.Invoking(p => p.GetElementInfo(null!)).Should().Throw(); + pr.Invoking(p => p.GetNextElements(currentElement)).Should().Throw(); } [Fact] @@ -254,7 +242,7 @@ public void GetOutgoingSequenceFlows_returns_SequenceFlow_objects_for_outgoing_f } }); } - + [Fact] public void GetOutgoingSequenceFlows_returns_SequenceFlow_objects_for_outgoing_flows_from_Gateway() { @@ -279,7 +267,7 @@ public void GetOutgoingSequenceFlows_returns_SequenceFlow_objects_for_outgoing_f } }); } - + [Fact] public void GetOutgoingSequenceFlows_returns_empty_list_when_no_outgoing() { @@ -288,102 +276,6 @@ public void GetOutgoingSequenceFlows_returns_empty_list_when_no_outgoing() List outgoingFLows = pr.GetOutgoingSequenceFlows(pr.GetFlowElement("EndEvent")); outgoingFLows.Should().BeEmpty(); } - - [Fact] - public void GetSequenceFlowsBetween_returns_all_sequenceflows_between_StartEvent_and_Task1() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "StartEvent"; - var nextElementId = "Task1"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEquivalentTo("Flow1"); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_all_sequenceflows_between_Task1_and_Task2() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "Task1"; - var nextElementId = "Task2"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEquivalentTo("Flow2", "Flow3"); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_all_sequenceflows_between_Task1_and_EndEvent() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "Task1"; - var nextElementId = "EndEvent"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEquivalentTo("Flow2", "Flow4"); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_all_sequenceflows_between_Task1_and_EndEvent_complex() - { - var bpmnfile = "simple-gateway-with-join-gateway.bpmn"; - var currentElement = "Task1"; - var nextElementId = "EndEvent"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEquivalentTo("Flow2", "Flow4", "Flow6"); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_empty_list_when_unknown_target() - { - var bpmnfile = "simple-gateway-default.bpmn"; - var currentElement = "Task1"; - var nextElementId = "Foobar"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEmpty(); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_empty_list_when_current_is_null() - { - var bpmnfile = "simple-gateway-default.bpmn"; - string? currentElement = null; - var nextElementId = "Foobar"; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEmpty(); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_empty_list_when_next_is_null() - { - var bpmnfile = "simple-gateway-default.bpmn"; - string currentElement = "Task1"; - string? nextElementId = null; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEmpty(); - } - - [Fact] - public void GetSequenceFlowsBetween_returns_empty_list_when_current_and_next_is_null() - { - var bpmnfile = "simple-gateway-default.bpmn"; - string? currentElement = null; - string? nextElementId = null; - ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - var actual = pr.GetSequenceFlowsBetween(currentElement, nextElementId); - var returnedIds = actual.Select(s => s.Id).ToList(); - returnedIds.Should().BeEmpty(); - } [Fact] public void Constructor_Fails_if_invalid_bpmn() @@ -404,7 +296,7 @@ public void GetFlowElement_returns_StartEvent_with_id() Outgoing = new List { "Flow1" } }); } - + [Fact] public void GetFlowElement_returns_ProcessTask_with_id() { @@ -415,10 +307,24 @@ public void GetFlowElement_returns_ProcessTask_with_id() Id = "Task1", Name = null!, Incoming = new List { "Flow1" }, - Outgoing = new List { "Flow2" } + Outgoing = new List { "Flow2" }, + ExtensionElements = new ExtensionElements() + { + AltinnProperties = new AltinnProperties() + { + AltinnActions = new List() + { + new() + { + Id = "submit", + } + }, + TaskType = "data" + } + } }); } - + [Fact] public void GetFlowElement_returns_EndEvent_with_id() { @@ -432,7 +338,7 @@ public void GetFlowElement_returns_EndEvent_with_id() Outgoing = new List() }); } - + [Fact] public void GetFlowElement_returns_null_when_id_not_found() { @@ -440,7 +346,7 @@ public void GetFlowElement_returns_null_when_id_not_found() ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); pr.GetFlowElement("Foobar").Should().BeNull(); } - + [Fact] public void GetFlowElement_returns_Gateway_with_id() { diff --git a/test/Altinn.App.Core.Tests/Internal/Process/StubGatewayFilters/DataValuesFilter.cs b/test/Altinn.App.Core.Tests/Internal/Process/StubGatewayFilters/DataValuesFilter.cs index 9565fc8db..50bd024f7 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/StubGatewayFilters/DataValuesFilter.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/StubGatewayFilters/DataValuesFilter.cs @@ -1,3 +1,4 @@ +#nullable enable using System.Collections.Generic; using System.Threading.Tasks; using Altinn.App.Core.Features; @@ -18,7 +19,7 @@ public DataValuesFilter(string gatewayId, string filterOnDataValue) _filterOnDataValue = filterOnDataValue; } - public async Task> FilterAsync(List outgoingFlows, Instance instance) + public async Task> FilterAsync(List outgoingFlows, Instance instance, string? action) { var targetFlow = instance.DataValues[_filterOnDataValue]; return await Task.FromResult(outgoingFlows.FindAll(e => e.Id == targetFlow)); diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/ProcessEngineTest/config/process/process.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/ProcessEngineTest/config/process/process.bpmn index f28219543..6c389b3bc 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/ProcessEngineTest/config/process/process.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/ProcessEngineTest/config/process/process.bpmn @@ -11,9 +11,17 @@ targetNamespace="http://bpmn.io/schema/bpmn" > SequenceFlow_1n56yn5 - + SequenceFlow_1n56yn5 SequenceFlow_1oot28q + + + + + + + + SequenceFlow_1oot28q diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn index 45af6b661..a9ad1e9cb 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn @@ -1,27 +1,50 @@ - + Flow1 - + Flow1 Flow2 + + + + + + data + + - + Flow2 Flow3 Flow4 - - + + Flow3 Flow5 + + + + + + + confirm + + - + Flow5 Flow4 diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn index 441a4aa95..2c1f15152 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn @@ -1,33 +1,55 @@ - + Flow1 - + Flow1 Flow2 + + + + + + data + + - + Flow2 Flow3 Flow4 - - + + Flow3 Flow5 + + + + + + data + + - + - Flow4 - Flow5 - Flow6 - - + Flow4 + Flow5 + Flow6 + + Flow6 diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway.bpmn index ce5be22a4..d5839d503 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway.bpmn @@ -1,27 +1,50 @@ - + Flow1 - + Flow1 Flow2 + + + + + + + + - + Flow2 Flow3 Flow4 - - + + Flow3 Flow5 + + + + + + + + + - + Flow5 Flow4 diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-both.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-both.bpmn new file mode 100644 index 000000000..06dc5c065 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-both.bpmn @@ -0,0 +1,46 @@ + + + + + Flow1 + + + + Flow1 + Flow2 + + + + + + data2 + + + + + + Flow2 + Flow3 + + + + + + + confirmation2 + + + + + + Flow3 + + + diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-new.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-new.bpmn new file mode 100644 index 000000000..3422a65c8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-new.bpmn @@ -0,0 +1,46 @@ + + + + + Flow1 + + + + Flow1 + Flow2 + + + data + + + + + + + + + Flow2 + Flow3 + + + + + + + confirmation + + + + + + Flow3 + + + diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-no-end.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-no-end.bpmn index 94cc8a644..66b353c33 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-no-end.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-no-end.bpmn @@ -12,13 +12,23 @@ xmlns:altinn="http://altinn.no"> Flow1 - + Flow1 Flow2 + + + data + + - + Flow2 + + + confirmation + + diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs b/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs index a0483d88a..041bc7a36 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs @@ -1,14 +1,13 @@ -using System.IO; using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.Process; using Moq; -namespace Altinn.App.PlatformServices.Tests.Internal.Process.TestUtils; +namespace Altinn.App.Core.Tests.Internal.Process.TestUtils; internal static class ProcessTestUtils { private static readonly string TestDataPath = Path.Combine("Internal", "Process", "TestData"); - + internal static ProcessReader SetupProcessReader(string bpmnfile) { Mock processServiceMock = new Mock(); From 775006a481e76470c5a33ef75e49d8c8b94e4247 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Thu, 1 Jun 2023 13:46:59 +0200 Subject: [PATCH 02/46] #192 Support for expressions in BPMN gateways (#246) * Working gateway filter leveraging expressions to make decisions * remove logger * Add tests for expressions function gatewayAction * move test out of shared tests as it is only supported backend (no process in frontend) * Add tests and small refactoring * Fix code smells * Fixes after review --- .../Controllers/InstancesController.cs | 1 + .../Controllers/ProcessController.cs | 1 + .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Features/IProcessExclusiveGateway.cs | 7 +- .../Expressions/ExpressionEvaluator.cs | 5 + .../Expressions/LayoutEvaluatorState.cs | 13 +- .../LayoutEvaluatorStateInitializer.cs | 4 +- .../AltinnProperties.cs | 6 + .../Process/Elements/ExclusiveGateway.cs | 6 + .../Internal/Process/Elements/SequenceFlow.cs | 6 + .../Process/ExpressionsExclusiveGateway.cs | 169 ++++++++ .../Internal/Process/IProcessEngine.cs | 2 +- .../Internal/Process/ProcessEngine.cs | 2 +- .../Internal/Process/ProcessNavigator.cs | 27 +- .../Expressions/ExpressionFunctionEnum.cs | 4 + .../{ => Process}/ProcessChangeResult.cs | 2 +- .../Process/ProcessGatewayInformation.cs | 17 + .../Process/ProcessNextRequest.cs | 2 +- .../Process/ProcessStartRequest.cs | 2 +- .../{ => Process}/ProcessStateChange.cs | 4 +- .../InstancesController_CopyInstanceTests.cs | 2 +- .../Altinn.App.Core.Tests.csproj | 4 + .../ExpressionsExclusiveGatewayTests.cs | 385 ++++++++++++++++++ .../Internal/Process/ProcessEngineTest.cs | 2 +- .../Internal/Process/ProcessNavigatorTests.cs | 3 +- .../StubGatewayFilters/DataValuesFilter.cs | 11 +- .../Internal/Process/TestData/DummyModel.cs | 9 + .../CommonTests/ExpressionTestCaseRoot.cs | 3 + .../TestBackendExclusiveFunctions.cs | 140 +++++++ .../CommonTests/TestFunctions.cs | 2 +- .../no-action-defined-is-null.json | 22 + .../gatewayAction/simple-lookup-equals.json | 23 ++ .../gatewayAction/simple-lookup.json | 23 ++ 33 files changed, 881 insertions(+), 29 deletions(-) create mode 100644 src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs rename src/Altinn.App.Core/Models/{ => Process}/ProcessChangeResult.cs (96%) create mode 100644 src/Altinn.App.Core/Models/Process/ProcessGatewayInformation.cs rename src/Altinn.App.Core/{Internal => Models}/Process/ProcessNextRequest.cs (93%) rename src/Altinn.App.Core/{Internal => Models}/Process/ProcessStartRequest.cs (95%) rename src/Altinn.App.Core/Models/{ => Process}/ProcessStateChange.cs (90%) create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/TestData/DummyModel.cs create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/no-action-defined-is-null.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup-equals.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup.json diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index 2de5fffc0..b55f32465 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -17,6 +17,7 @@ using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.Validation; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Helpers; diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 6d6bdba3e..3bc0b8534 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -8,6 +8,7 @@ using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 0d33707a6..d94281dd8 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -225,6 +225,7 @@ private static void AddProcessServices(IServiceCollection services) services.TryAddTransient(); services.TryAddSingleton(); services.TryAddTransient(); + services.AddTransient(); services.TryAddTransient(); } } diff --git a/src/Altinn.App.Core/Features/IProcessExclusiveGateway.cs b/src/Altinn.App.Core/Features/IProcessExclusiveGateway.cs index 4ea37a384..047df0af7 100644 --- a/src/Altinn.App.Core/Features/IProcessExclusiveGateway.cs +++ b/src/Altinn.App.Core/Features/IProcessExclusiveGateway.cs @@ -1,4 +1,5 @@ using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Models.Process; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Features; @@ -18,7 +19,7 @@ public interface IProcessExclusiveGateway /// /// Complete list of defined flows out of gateway /// Instance where process is about to move next - /// Action performed by the requester - /// - public Task> FilterAsync(List outgoingFlows, Instance instance, string? action); + /// Information connected with the current gateway under evaluation + /// List of possible SequenceFlows to choose out of the gateway + public Task> FilterAsync(List outgoingFlows, Instance instance, ProcessGatewayInformation processGatewayInformation); } diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs index f2afe3651..7434b1a6b 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs @@ -76,6 +76,7 @@ public static bool EvaluateBooleanExpression(LayoutEvaluatorState state, Compone ExpressionFunction.and => And(args), ExpressionFunction.or => Or(args), ExpressionFunction.not => Not(args), + ExpressionFunction.gatewayAction => state.GetGatewayAction(), _ => throw new ExpressionEvaluatorTypeErrorException($"Function \"{expr.Function}\" not implemented"), }; return ret; @@ -191,6 +192,8 @@ private static (double?, double?) PrepareNumericArgs(object?[] args) { bool ab => throw new ExpressionEvaluatorTypeErrorException($"Expected number, got value {(ab ? "true" : "false")}"), string s => parseNumber(s), + decimal d => (double)d, + int i => (double)i, object o => o as double?, // assume all relevant numers are representable as double (as in frontend) _ => null, }; @@ -199,6 +202,8 @@ private static (double?, double?) PrepareNumericArgs(object?[] args) { bool bb => throw new ExpressionEvaluatorTypeErrorException($"Expected number, got value {(bb ? "true" : "false")}"), string s => parseNumber(s), + decimal d => (double)d, + int i => (double)i, object o => o as double?, // assume all relevant numers are representable as double (as in frontend) _ => null, }; diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs index 729d6a8b6..5f7ce8866 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs @@ -16,16 +16,18 @@ public class LayoutEvaluatorState private readonly LayoutModel _componentModel; private readonly FrontEndSettings _frontEndSettings; private readonly Instance _instanceContext; + private readonly string? _gatewayAction; /// /// Constructor for LayoutEvaluatorState. Usually called via that can be fetched from dependency injection. /// - public LayoutEvaluatorState(IDataModelAccessor dataModel, LayoutModel componentModel, FrontEndSettings frontEndSettings, Instance instance) + public LayoutEvaluatorState(IDataModelAccessor dataModel, LayoutModel componentModel, FrontEndSettings frontEndSettings, Instance instance, string? gatewayAction = null) { _dataModel = dataModel; _componentModel = componentModel; _frontEndSettings = frontEndSettings; _instanceContext = instance; + _gatewayAction = gatewayAction; } @@ -184,6 +186,15 @@ public string GetInstanceContext(string key) }; } + /// + /// Get the gateway action from the instance context + /// + /// Returns null if no action defined + public string? GetGatewayAction() + { + return _gatewayAction; + } + /// /// Return a full dataModelBiding from a context aware binding by adding indicies /// diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs index cc401cf3b..db51ff9d8 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs @@ -29,9 +29,9 @@ public LayoutEvaluatorStateInitializer(IAppResources appResources, IOptions /// Initialize LayoutEvaluatorState with given Instance, data object and layoutSetId /// - public Task Init(Instance instance, object data, string? layoutSetId) + public Task Init(Instance instance, object data, string? layoutSetId, string? gatewayAction = null) { var layouts = _appResources.GetLayoutModel(layoutSetId); - return Task.FromResult(new LayoutEvaluatorState(new DataModel(data), layouts, _frontEndSettings, instance)); + return Task.FromResult(new LayoutEvaluatorState(new DataModel(data), layouts, _frontEndSettings, instance, gatewayAction)); } } \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnProperties.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnProperties.cs index 55b776202..f2f90bde1 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnProperties.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnProperties.cs @@ -20,5 +20,11 @@ public class AltinnProperties //[XmlElement(ElementName = "taskType", Namespace = "http://altinn.no", IsNullable = true)] [XmlElement("taskType", Namespace = "http://altinn.no")] public string? TaskType { get; set; } + + /// + /// Gets or sets the data type id connected to the task + /// + [XmlElement("connectedDataTypeId", Namespace = "http://altinn.no", IsNullable = true)] + public string? ConnectedDataTypeId { get; set; } } } \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ExclusiveGateway.cs b/src/Altinn.App.Core/Internal/Process/Elements/ExclusiveGateway.cs index 0379ca56d..d108f28d4 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ExclusiveGateway.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ExclusiveGateway.cs @@ -13,6 +13,12 @@ public class ExclusiveGateway: ProcessElement /// [XmlAttribute("default")] public string? Default { get; set; } + + /// + /// + /// + [XmlElement("extensionElements")] + public ExtensionElements? ExtensionElements { get; set; } /// /// String representation of process element type diff --git a/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs b/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs index 36646e73a..809b44b41 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/SequenceFlow.cs @@ -30,5 +30,11 @@ public class SequenceFlow /// [XmlAttribute("flowtype", Namespace = "http://altinn.no")] public string FlowType { get; set; } + + /// + /// Gets or sets the condition expression of a sequence flow + /// + [XmlElement("conditionExpression")] + public string? ConditionExpression { get; set; } } } diff --git a/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs b/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs new file mode 100644 index 000000000..de39c1fa8 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs @@ -0,0 +1,169 @@ +using System.Text; +using System.Text.Json; +using Altinn.App.Core.Features; +using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Expressions; +using Altinn.App.Core.Models.Process; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process +{ + /// + /// Class implementing for evaluating expressions on flows connected to a gateway + /// + public class ExpressionsExclusiveGateway : IProcessExclusiveGateway + { + private readonly LayoutEvaluatorStateInitializer _layoutStateInit; + private readonly IAppResources _resources; + private readonly IAppMetadata _appMetadata; + private readonly IData _dataClient; + private readonly IAppModel _appModel; + + /// + /// Constructor for + /// + /// Expressions state initalizer used to create context for expression evaluation + /// Service for fetching app resources + /// Service for fetching app model + /// Service for fetching app metadata + /// Service for interacting with Platform Storage + public ExpressionsExclusiveGateway( + LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, + IAppResources resources, + IAppModel appModel, + IAppMetadata appMetadata, + IData dataClient) + { + _layoutStateInit = layoutEvaluatorStateInitializer; + _resources = resources; + _appMetadata = appMetadata; + _dataClient = dataClient; + _appModel = appModel; + } + + /// + public string GatewayId { get; } = "AltinnExpressionsExclusiveGateway"; + + /// + public async Task> FilterAsync(List outgoingFlows, Instance instance, ProcessGatewayInformation processGatewayInformation) + { + var state = await GetLayoutEvaluatorState(instance, processGatewayInformation.Action, processGatewayInformation.DataTypeId); + + return outgoingFlows.Where(outgoingFlow => EvaluateSequenceFlow(state, outgoingFlow)).ToList(); + } + + private async Task GetLayoutEvaluatorState(Instance instance, string? action, string? dataTypeId) + { + var layoutSet = GetLayoutSet(instance); + var (checkedDataTypeId, dataType) = await GetDataType(instance, layoutSet, dataTypeId); + object data = new object(); + if (checkedDataTypeId != null && dataType != null) + { + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instance); + var dataGuid = GetDataId(instance, checkedDataTypeId); + Type dataElementType = dataType; + if (dataGuid != null) + { + data = await _dataClient.GetFormData(instanceIdentifier.InstanceGuid, dataElementType, instance.Org, instance.AppId.Split("/")[1], int.Parse(instance.InstanceOwner.PartyId), dataGuid.Value); + } + } + + var state = await _layoutStateInit.Init(instance, data, layoutSetId: layoutSet?.Id, gatewayAction: action); + return state; + } + + private bool EvaluateSequenceFlow(LayoutEvaluatorState state, SequenceFlow sequenceFlow) + { + if (sequenceFlow.ConditionExpression != null) + { + var expression = GetExpressionFromCondition(sequenceFlow.ConditionExpression); + foreach (var componentContext in state.GetComponentContexts()) + { + var result = ExpressionEvaluator.EvaluateExpression(state, expression, componentContext); + if (result is bool boolResult && boolResult) + { + return true; + } + } + } + else + { + return true; + } + + return false; + } + + private static Expression GetExpressionFromCondition(string condition) + { + JsonSerializerOptions options = new() + { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNameCaseInsensitive = true, + }; + Utf8JsonReader reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(condition)); + reader.Read(); + var expressionFromCondition = ExpressionConverter.ReadNotNull(ref reader, options); + return expressionFromCondition; + } + + private LayoutSet? GetLayoutSet(Instance instance) + { + string taskId = instance.Process.CurrentTask.ElementId; + JsonSerializerOptions options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + + string layoutSetsString = _resources.GetLayoutSets(); + LayoutSet? layoutSet = null; + if (!string.IsNullOrEmpty(layoutSetsString)) + { + LayoutSets? layoutSets = JsonSerializer.Deserialize(layoutSetsString, options)!; + layoutSet = layoutSets?.Sets?.FirstOrDefault(t => t.Tasks.Contains(taskId)); + } + + return layoutSet; + } + + //TODO: Find a better home for this method + private async Task<(string? DataTypeId, Type? DataTypeClassType)> GetDataType(Instance instance, LayoutSet? layoutSet, string? dataTypeId) + { + DataType? dataType; + if (dataTypeId != null) + { + dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.FirstOrDefault(d => d.Id == dataTypeId && d.AppLogic != null); + } + else if (layoutSet != null) + { + dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.FirstOrDefault(d => d.Id == layoutSet.DataType && d.AppLogic != null); + } + else + { + dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.FirstOrDefault(d => d.TaskId == instance.Process.CurrentTask.ElementId && d.AppLogic != null); + } + + if (dataType != null) + { + return (dataType.Id, _appModel.GetModelType(dataType.AppLogic.ClassRef)); + } + + return (null, null); + } + + private static Guid? GetDataId(Instance instance, string dataType) + { + string? dataId = instance.Data.FirstOrDefault(d => d.DataType == dataType)?.Id; + if (dataId != null) + { + return new Guid(dataId); + } + + return null; + } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/IProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/IProcessEngine.cs index 05ef332e1..bb62df68a 100644 --- a/src/Altinn.App.Core/Internal/Process/IProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/IProcessEngine.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Process; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index eb8b7e022..7e550c62d 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -4,7 +4,7 @@ using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.Base; -using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Process; using Altinn.Platform.Profile.Models; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; diff --git a/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs b/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs index 6f82781fe..5b2b9af33 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs @@ -1,7 +1,9 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Models.Process; using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; namespace Altinn.App.Core.Internal.Process; @@ -12,16 +14,19 @@ public class ProcessNavigator : IProcessNavigator { private readonly IProcessReader _processReader; private readonly ExclusiveGatewayFactory _gatewayFactory; + private readonly ILogger _logger; /// /// Initialize a new instance of /// /// The process reader /// Service to fetch wanted gateway filter implementation - public ProcessNavigator(IProcessReader processReader, ExclusiveGatewayFactory gatewayFactory) + /// The logger + public ProcessNavigator(IProcessReader processReader, ExclusiveGatewayFactory gatewayFactory, ILogger logger) { _processReader = processReader; _gatewayFactory = gatewayFactory; + _logger = logger; } @@ -60,8 +65,15 @@ private async Task> NextFollowAndFilterGateways(Instance in } var gateway = (ExclusiveGateway)directFlowTarget; - IProcessExclusiveGateway? gatewayFilter = _gatewayFactory.GetProcessExclusiveGateway(directFlowTarget.Id); List outgoingFlows = _processReader.GetOutgoingSequenceFlows(directFlowTarget); + IProcessExclusiveGateway? gatewayFilter = null; + if(outgoingFlows.Any(a => a.ConditionExpression != null)) + { + gatewayFilter = _gatewayFactory.GetProcessExclusiveGateway("AltinnExpressionsExclusiveGateway"); + } else + { + gatewayFilter = _gatewayFactory.GetProcessExclusiveGateway(directFlowTarget.Id); + } List filteredList; if (gatewayFilter == null) { @@ -69,9 +81,14 @@ private async Task> NextFollowAndFilterGateways(Instance in } else { - filteredList = await gatewayFilter.FilterAsync(outgoingFlows, instance, action); + ProcessGatewayInformation gatewayInformation = new() + { + Action = action, + DataTypeId = gateway.ExtensionElements?.AltinnProperties?.ConnectedDataTypeId + }; + + filteredList = await gatewayFilter.FilterAsync(outgoingFlows, instance, gatewayInformation); } - var defaultSequenceFlow = filteredList.Find(s => s.Id == gateway.Default); if (defaultSequenceFlow != null) { @@ -84,7 +101,7 @@ private async Task> NextFollowAndFilterGateways(Instance in filteredNext.AddRange(await NextFollowAndFilterGateways(instance, filteredTargets, action)); } } - + _logger.LogDebug("Filtered next elements: {FilteredNextElements}", string.Join(", ", filteredNext.Select(e => e.Id))); return filteredNext; } diff --git a/src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs b/src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs index f5eec0e9e..3d91e4f6a 100644 --- a/src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs +++ b/src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs @@ -69,4 +69,8 @@ public enum ExpressionFunction /// Return true if the single argument evaluate to false, otherwise return false /// not, + /// + /// Get the action performed in task prior to bpmn gateway + /// + gatewayAction, } \ No newline at end of file diff --git a/src/Altinn.App.Core/Models/ProcessChangeResult.cs b/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs similarity index 96% rename from src/Altinn.App.Core/Models/ProcessChangeResult.cs rename to src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs index 68aa1736c..d7981f48b 100644 --- a/src/Altinn.App.Core/Models/ProcessChangeResult.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models.Process { /// /// Class representing the result of a process change diff --git a/src/Altinn.App.Core/Models/Process/ProcessGatewayInformation.cs b/src/Altinn.App.Core/Models/Process/ProcessGatewayInformation.cs new file mode 100644 index 000000000..ca6f13d03 --- /dev/null +++ b/src/Altinn.App.Core/Models/Process/ProcessGatewayInformation.cs @@ -0,0 +1,17 @@ +namespace Altinn.App.Core.Models.Process; + +/// +/// Additional information about the gateway in the context of a running process +/// +public class ProcessGatewayInformation +{ + /// + /// The action performed to reach the gateway + /// + public string? Action { get; set; } + + /// + /// The datatype associated with the gateway + /// + public string? DataTypeId { get; set; } +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessNextRequest.cs b/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs similarity index 93% rename from src/Altinn.App.Core/Internal/Process/ProcessNextRequest.cs rename to src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs index 18ebe02cc..5f02ca231 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessNextRequest.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs @@ -1,7 +1,7 @@ using System.Security.Claims; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process; +namespace Altinn.App.Core.Models.Process; /// /// Class that defines the request for moving the process to the next task diff --git a/src/Altinn.App.Core/Internal/Process/ProcessStartRequest.cs b/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs similarity index 95% rename from src/Altinn.App.Core/Internal/Process/ProcessStartRequest.cs rename to src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs index 3303913fa..bfa6c1642 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessStartRequest.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessStartRequest.cs @@ -1,7 +1,7 @@ using System.Security.Claims; using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Internal.Process; +namespace Altinn.App.Core.Models.Process; /// /// Class that defines the request for starting a new process diff --git a/src/Altinn.App.Core/Models/ProcessStateChange.cs b/src/Altinn.App.Core/Models/Process/ProcessStateChange.cs similarity index 90% rename from src/Altinn.App.Core/Models/ProcessStateChange.cs rename to src/Altinn.App.Core/Models/Process/ProcessStateChange.cs index 8232857fc..fcca5ac3c 100644 --- a/src/Altinn.App.Core/Models/ProcessStateChange.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessStateChange.cs @@ -1,8 +1,6 @@ -using System.Collections.Generic; - using Altinn.Platform.Storage.Interface.Models; -namespace Altinn.App.Core.Models +namespace Altinn.App.Core.Models.Process { /// /// Represents a change in process state for an instance. diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs index ae759f000..84ac2ac03 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs @@ -6,8 +6,8 @@ using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; -using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.Validation; using Altinn.Authorization.ABAC.Xacml.JsonProfile; diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index a31dce338..653f6b5dd 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -85,6 +85,10 @@ + + + + ..\..\Altinn3.ruleset diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs new file mode 100644 index 000000000..9e1ae4ccd --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs @@ -0,0 +1,385 @@ +#nullable enable +using System.Text.Json; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; +using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Layout; +using Altinn.App.Core.Models.Layout.Components; +using Altinn.App.Core.Models.Process; +using Altinn.App.Core.Tests.Internal.Process.TestData; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process; + +public class ExpressionsExclusiveGatewayTests +{ + [Fact] + public async Task FilterAsync_NoExpressions_ReturnsAllFlows() + { + // Arrange + List dataTypes = new List() + { + new() + { + Id = "test", + AppLogic = new() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.TestData.DummyModel", + } + } + }; + IProcessExclusiveGateway gateway = SetupExpressionsGateway(dataTypes: dataTypes); + var outgoingFlows = new List + { + new SequenceFlow + { + Id = "1", + ConditionExpression = null, + }, + new SequenceFlow + { + Id = "2", + ConditionExpression = null, + }, + }; + var instance = new Instance() + { + Id = "500000/60226acd-b821-4aae-82cd-97a342071bd3", + InstanceOwner = new() + { + PartyId = "500000" + }, + AppId = "ttd/test", + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + }, + Data = new() + { + new() + { + Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", + DataType = "test" + } + } + }; + var processGatewayInformation = new ProcessGatewayInformation + { + Action = "confirm", + }; + + // Act + var result = await gateway.FilterAsync(outgoingFlows, instance, processGatewayInformation); + + // Assert + Assert.Equal(2, result.Count); + Assert.Equal("1", result[0].Id); + Assert.Equal("2", result[1].Id); + } + + [Fact] + public async Task FilterAsync_Expression_filters_based_on_action() + { + // Arrange + List dataTypes = new List() + { + new() + { + Id = "test", + AppLogic = new() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.TestData.DummyModel", + } + } + }; + IProcessExclusiveGateway gateway = SetupExpressionsGateway(dataTypes: dataTypes); + var outgoingFlows = new List + { + new SequenceFlow + { + Id = "1", + ConditionExpression = "[\"equals\", [\"gatewayAction\"], \"confirm\"]", + }, + new SequenceFlow + { + Id = "2", + ConditionExpression = "[\"equals\", [\"gatewayAction\"], \"reject\"]", + }, + }; + var instance = new Instance() + { + Id = "500000/60226acd-b821-4aae-82cd-97a342071bd3", + InstanceOwner = new() + { + PartyId = "500000" + }, + AppId = "ttd/test", + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + }, + Data = new() + { + new() + { + Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", + DataType = "test" + } + } + }; + var processGatewayInformation = new ProcessGatewayInformation + { + Action = "confirm", + }; + + // Act + var result = await gateway.FilterAsync(outgoingFlows, instance, processGatewayInformation); + + // Assert + Assert.Single(result); + Assert.Equal("1", result[0].Id); + } + + [Fact] + public async Task FilterAsync_Expression_filters_based_on_datamodel_set_by_layoutset() + { + // Arrange + List dataTypes = new List() + { + new() + { + Id = "aa", + AppLogic = new() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.TestData.NotFound", + } + }, + new() + { + Id = "test", + AppLogic = new() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.TestData.DummyModel", + } + } + }; + object formData = new DummyModel() + { + Amount = 1000, + Submitter = "test" + }; + LayoutSets layoutSets = new LayoutSets() + { + Sets = new() + { + new() + { + Id = "test", + Tasks = new() { "Task_1" }, + DataType = "test" + } + } + }; + IProcessExclusiveGateway gateway = SetupExpressionsGateway(dataTypes: dataTypes, formData: formData, layoutSets: LayoutSetsToString(layoutSets), dataType: formData.GetType()); + var outgoingFlows = new List + { + new SequenceFlow + { + Id = "1", + ConditionExpression = "[\"notEquals\", [\"dataModel\", \"Amount\"], 1000]", + }, + new SequenceFlow + { + Id = "2", + ConditionExpression = "[\"equals\", [\"dataModel\", \"Amount\"], 1000]", + }, + }; + var instance = new Instance() + { + Id = "500000/60226acd-b821-4aae-82cd-97a342071bd3", + InstanceOwner = new() + { + PartyId = "500000" + }, + AppId = "ttd/test", + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + }, + Data = new() + { + new() + { + Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", + DataType = "test" + } + } + }; + var processGatewayInformation = new ProcessGatewayInformation + { + Action = "confirm", + }; + + // Act + var result = await gateway.FilterAsync(outgoingFlows, instance, processGatewayInformation); + + // Assert + Assert.Single(result); + Assert.Equal("2", result[0].Id); + } + + [Fact] + public async Task FilterAsync_Expression_filters_based_on_datamodel_set_by_gateway() + { + // Arrange + List dataTypes = new List() + { + new() + { + Id = "aa", + AppLogic = new() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.TestData.DummyModel", + } + }, + new() + { + Id = "test", + AppLogic = new() + { + ClassRef = "Altinn.App.Core.Tests.Internal.Process.TestData.NotFound", + } + } + }; + object formData = new DummyModel() + { + Amount = 1000, + Submitter = "test" + }; + LayoutSets layoutSets = new LayoutSets() + { + Sets = new() + { + new() + { + Id = "test", + Tasks = new() { "Task_1" }, + DataType = "test" + } + } + }; + IProcessExclusiveGateway gateway = SetupExpressionsGateway(dataTypes: dataTypes, formData: formData, layoutSets: LayoutSetsToString(layoutSets), dataType: formData.GetType()); + var outgoingFlows = new List + { + new SequenceFlow + { + Id = "1", + ConditionExpression = "[\"notEquals\", [\"dataModel\", \"Amount\"], 1000]", + }, + new SequenceFlow + { + Id = "2", + ConditionExpression = "[\"equals\", [\"dataModel\", \"Amount\"], 1000]", + }, + }; + var instance = new Instance() + { + Id = "500000/60226acd-b821-4aae-82cd-97a342071bd3", + InstanceOwner = new() + { + PartyId = "500000" + }, + AppId = "ttd/test", + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + }, + Data = new() + { + new() + { + Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", + DataType = "aa" + } + } + }; + var processGatewayInformation = new ProcessGatewayInformation + { + Action = "confirm", + DataTypeId = "aa" + }; + + // Act + var result = await gateway.FilterAsync(outgoingFlows, instance, processGatewayInformation); + + // Assert + Assert.Single(result); + Assert.Equal("2", result[0].Id); + } + + private static ExpressionsExclusiveGateway SetupExpressionsGateway(List dataTypes, string? layoutSets = null, object? formData = null, Type? dataType = null) + { + var resources = new Mock(); + var appModel = new Mock(); + var appMetadata = new Mock(); + var dataClient = new Mock(); + + resources.Setup(r => r.GetLayoutSets()).Returns(layoutSets ?? string.Empty); + appMetadata.Setup(m => m.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata("ttd/test-app") + { + DataTypes = dataTypes + }); + resources.Setup(r => r.GetLayoutModel(It.IsAny())).Returns(new LayoutModel() + { + Pages = new Dictionary() + { + { + "Page1", new("Page1", new List(), new Dictionary(), null, null, null, null) + } + } + }); + if (formData != null) + { + dataClient.Setup(d => d.GetFormData(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(formData); + } + + if (dataType != null) + { + appModel.Setup(a => a.GetModelType(dataType.FullName!)).Returns(dataType); + } + + var frontendSettings = Options.Create(new FrontEndSettings()); + var layoutStateInit = new LayoutEvaluatorStateInitializer(resources.Object, frontendSettings); + return new ExpressionsExclusiveGateway(layoutStateInit, resources.Object, appModel.Object, appMetadata.Object, dataClient.Object); + } + + private static string LayoutSetsToString(LayoutSets layoutSets) + { + return JsonSerializer.Serialize(layoutSets, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + }); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index 30f4cf6a6..7fa228550 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -4,7 +4,7 @@ using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Process; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Enums; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs index 22fdc9519..f9e150271 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs @@ -6,6 +6,7 @@ using Altinn.App.PlatformServices.Tests.Internal.Process.StubGatewayFilters; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Altinn.App.Core.Tests.Internal.Process; @@ -185,6 +186,6 @@ public async void GetNextTask_returns_empty_list_if_element_has_no_next() private static IProcessNavigator SetupProcessNavigator(string bpmnfile, IEnumerable gatewayFilters) { ProcessReader pr = ProcessTestUtils.SetupProcessReader(bpmnfile); - return new ProcessNavigator(pr, new ExclusiveGatewayFactory(gatewayFilters)); + return new ProcessNavigator(pr, new ExclusiveGatewayFactory(gatewayFilters), new NullLogger()); } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/StubGatewayFilters/DataValuesFilter.cs b/test/Altinn.App.Core.Tests/Internal/Process/StubGatewayFilters/DataValuesFilter.cs index 50bd024f7..04709370c 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/StubGatewayFilters/DataValuesFilter.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/StubGatewayFilters/DataValuesFilter.cs @@ -1,16 +1,15 @@ #nullable enable -using System.Collections.Generic; -using System.Threading.Tasks; using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Models.Process; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.PlatformServices.Tests.Internal.Process.StubGatewayFilters; -public class DataValuesFilter: IProcessExclusiveGateway +public class DataValuesFilter : IProcessExclusiveGateway { public string GatewayId { get; } - + private readonly string _filterOnDataValue; public DataValuesFilter(string gatewayId, string filterOnDataValue) @@ -18,8 +17,8 @@ public DataValuesFilter(string gatewayId, string filterOnDataValue) GatewayId = gatewayId; _filterOnDataValue = filterOnDataValue; } - - public async Task> FilterAsync(List outgoingFlows, Instance instance, string? action) + + public async Task> FilterAsync(List outgoingFlows, Instance instance, ProcessGatewayInformation processGatewayInformation) { var targetFlow = instance.DataValues[_filterOnDataValue]; return await Task.FromResult(outgoingFlows.FindAll(e => e.Id == targetFlow)); diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/DummyModel.cs b/test/Altinn.App.Core.Tests/Internal/Process/TestData/DummyModel.cs new file mode 100644 index 000000000..a2b5bd380 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/DummyModel.cs @@ -0,0 +1,9 @@ +namespace Altinn.App.Core.Tests.Internal.Process.TestData +{ + public class DummyModel + { + public string Submitter { get; set; } + + public decimal Amount { get; set; } + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs index 4a484637b..34ec008ec 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs @@ -53,6 +53,9 @@ public class ExpressionTestCaseRoot [JsonPropertyName("instance")] public Instance? Instance { get; set; } + + [JsonPropertyName("gatewayAction")] + public string? GatewayAction { get; set; } public override string ToString() { diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs new file mode 100644 index 000000000..66833b0d4 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs @@ -0,0 +1,140 @@ +#nullable enable +using System.Reflection; +using System.Text.Json; + +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Tests.Helpers; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Altinn.App.Core.Tests.LayoutExpressions; + +public class TestBackendExclusiveFunctions +{ + private readonly ITestOutputHelper _output; + + public TestBackendExclusiveFunctions(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [ExclusiveTest("gatewayAction")] + public void GatewayAction_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + + private void RunTestCase(ExpressionTestCaseRoot test) + { + _output.WriteLine($"{test.Filename} in {test.Folder}"); + _output.WriteLine(test.RawJson); + _output.WriteLine(test.FullPath); + var state = new LayoutEvaluatorState( + new JsonDataModel(test.DataModel), + test.ComponentModel, + test.FrontEndSettings ?? new(), + test.Instance ?? new(), + test.GatewayAction); + + if (test.ExpectsFailure is not null) + { + if (test.ParsingException is not null) + { + test.ParsingException.Message.Should().Be(test.ExpectsFailure); + } + else + { + Action act = () => + { + ExpressionEvaluator.EvaluateExpression(state, test.Expression, test.Context?.ToContext(test.ComponentModel)!); + }; + act.Should().Throw().WithMessage(test.ExpectsFailure); + } + + return; + } + + test.ParsingException.Should().BeNull("Loading of test failed"); + + var result = ExpressionEvaluator.EvaluateExpression(state, test.Expression, test.Context?.ToContext(test.ComponentModel)!); + + switch (test.Expects.ValueKind) + { + case JsonValueKind.String: + result.Should().Be(test.Expects.GetString()); + break; + case JsonValueKind.True: + result.Should().Be(true); + break; + case JsonValueKind.False: + result.Should().Be(false); + break; + case JsonValueKind.Null: + result.Should().Be(null); + break; + case JsonValueKind.Number: + result.Should().Be(test.Expects.GetDouble()); + break; + case JsonValueKind.Undefined: + + default: + // Compare serialized json result for object and array + JsonSerializer.Serialize(result).Should().Be(JsonSerializer.Serialize(test.Expects)); + break; + } + } + + [Fact] + public void Ensure_tests_For_All_Folders() + { + // This is just a way to ensure that all folders have test methods associcated. + var jsonTestFolders = Directory.GetDirectories(Path.Join("LayoutExpressions", "CommonTests", "exclusive-tests", "functions")).Select(d => Path.GetFileName(d)).ToArray(); + var testMethods = this.GetType().GetMethods().Select(m => m.CustomAttributes.FirstOrDefault(ca => ca.AttributeType == typeof(ExclusiveTestAttribute))?.ConstructorArguments.FirstOrDefault().Value).OfType().ToArray(); + testMethods.Should().BeEquivalentTo(jsonTestFolders, "Shared test folders should have a corresponding test method"); + } +} + +public class ExclusiveTestAttribute : DataAttribute +{ + private readonly string _folder; + + public ExclusiveTestAttribute(string folder) + { + _folder = folder; + } + + public override IEnumerable GetData(MethodInfo methodInfo) + { + var files = Directory.GetFiles(Path.Join("LayoutExpressions", "CommonTests", "exclusive-tests", "functions", _folder)); + foreach (var file in files) + { + ExpressionTestCaseRoot testCase = new(); + var data = File.ReadAllText(file); + try + { + testCase = JsonSerializer.Deserialize( + data, + new JsonSerializerOptions + { + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + })!; + } + catch (Exception e) + { + using var jsonDocument = JsonDocument.Parse(data); + + testCase.Name = jsonDocument.RootElement.GetProperty("name").GetString(); + testCase.ExpectsFailure = jsonDocument.RootElement.TryGetProperty("expectsFailure", out var expectsFailure) ? expectsFailure.GetString() : null; + testCase.ParsingException = e; + } + + testCase.Filename = Path.GetFileName(file); + testCase.FullPath = file; + testCase.Folder = _folder; + testCase.RawJson = data; + + yield return new object[] { testCase }; + } + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs index 585e1ff79..4e4d0dc91 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs @@ -79,7 +79,7 @@ public TestFunctions(ITestOutputHelper output) [Theory] [SharedTest("or")] public void Or_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); - + [Theory] [SharedTest("unknown")] public void Unknown_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/no-action-defined-is-null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/no-action-defined-is-null.json new file mode 100644 index 000000000..a172bd2bb --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/no-action-defined-is-null.json @@ -0,0 +1,22 @@ +{ + "name": "Simple lookup", + "expression": ["gatewayAction"], + "expects": null, + "layouts": { + "Page1": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "current-component", + "type": "Paragraph" + } + ] + } + } + }, + "context": { + "component": "current-component", + "currentLayout": "Page1" + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup-equals.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup-equals.json new file mode 100644 index 000000000..625a6539d --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup-equals.json @@ -0,0 +1,23 @@ +{ + "name": "Simple lookup", + "expression": ["equals",["gatewayAction"], "sign"], + "expects": true, + "gatewayAction": "sign", + "layouts": { + "Page1": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "current-component", + "type": "Paragraph" + } + ] + } + } + }, + "context": { + "component": "current-component", + "currentLayout": "Page1" + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup.json new file mode 100644 index 000000000..f50b1dc38 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/exclusive-tests/functions/gatewayAction/simple-lookup.json @@ -0,0 +1,23 @@ +{ + "name": "Simple lookup", + "expression": ["gatewayAction"], + "expects": "sign", + "gatewayAction": "sign", + "layouts": { + "Page1": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "current-component", + "type": "Paragraph" + } + ] + } + } + }, + "context": { + "component": "current-component", + "currentLayout": "Page1" + } +} From 95b2a65605445d98c04e13319d486cd3b2324996 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Fri, 9 Jun 2023 10:35:14 +0200 Subject: [PATCH 03/46] Run extra code if action sign exiting action from a Task (#255) * Add a hook in the process engine where custom code kan be injected when a given action is performed * Refactor to UserAction Add tests for UserActionFactory * Add SignClient * Chore/refactor interfaces and clients (#253) * Huge refactoring * Moved all interfaces from Altinn.App.Interfaces to namespaces under Altinn.App.Internal * Interfaces for clients has postfix Client * Marked all old interfaces as obsolete with message of what interface to use instead * Actually add back the old interfaces * Fixes after review * Add tests for SignClient * Remove dependency to Altinn.Platform.Storage.Interface nuget from API-project as it is included in Core-project which API-project depends on * Implement signign action handler * Fixes after review * Fix codeQL notice and add test --- src/Altinn.App.Api/Altinn.App.Api.csproj | 1 - .../Controllers/AuthenticationController.cs | 8 +- .../Controllers/AuthorizationController.cs | 16 +- .../Controllers/DataController.cs | 12 +- .../Controllers/DataTagsController.cs | 16 +- .../Controllers/FileScanController.cs | 12 +- .../Controllers/HomeController.cs | 1 - .../Controllers/InstancesController.cs | 50 ++--- .../Controllers/PagesController.cs | 6 +- .../Controllers/PartiesController.cs | 16 +- .../Controllers/PdfController.cs | 12 +- .../Controllers/ProcessController.cs | 37 ++-- .../Controllers/ProfileController.cs | 10 +- .../Controllers/ResourceController.cs | 4 +- .../Controllers/StatelessDataController.cs | 19 +- .../Controllers/StatelessPagesController.cs | 6 +- .../Controllers/TextsController.cs | 3 +- .../Controllers/ValidateController.cs | 7 +- src/Altinn.App.Core/Altinn.App.Core.csproj | 2 +- .../DefaultEFormidlingService.cs | 11 +- .../EformidlingStatusCheckEventHandler.cs | 4 - .../Extensions/ServiceCollectionExtensions.cs | 51 +++-- .../Features/Action/NullUserAction.cs | 18 ++ .../Features/Action/SigningUserAction.cs | 85 +++++++++ .../Features/Action/UserActionFactory.cs | 34 ++++ src/Altinn.App.Core/Features/IUserAction.cs | 10 + .../Features/PageOrder/DefaultPageOrder.cs | 2 +- .../Features/Validation/ValidationAppSI.cs | 19 +- src/Altinn.App.Core/Helpers/UserHelper.cs | 21 ++- .../Implementation/AppResourcesSI.cs | 1 - .../Implementation/DefaultAppEvents.cs | 11 +- .../Implementation/DefaultTaskEvents.cs | 13 +- .../Implementation/PersonService.cs | 30 --- .../Implementation/PrefillSI.cs | 19 +- .../Implementation/UserTokenProvider.cs | 2 +- .../Authentication/AuthenticationClient.cs | 4 +- .../Authorization/AuthorizationClient.cs | 4 +- .../Clients/Events/EventsClient.cs | 6 +- .../Clients/KeyVault/SecretsClient.cs | 4 +- .../Clients/KeyVault/SecretsLocalClient.cs | 4 +- .../Clients/Pdf/PdfGeneratorClient.cs | 3 +- .../Clients/Profile/ProfileClient.cs | 4 +- .../Profile/ProfileClientCachingDecorator.cs | 10 +- ...RegisterClient.cs => AltinnPartyClient.cs} | 35 +--- .../Clients/Register/PersonClient.cs | 7 +- .../Clients/Register/RegisterDSFClient.cs | 82 --------- .../Clients/Register/RegisterERClient.cs | 4 +- .../Clients/Storage/ApplicationClient.cs | 4 +- .../Clients/Storage/DataClient.cs | 8 +- .../Clients/Storage/InstanceClient.cs | 4 +- .../Clients/Storage/InstanceEventClient.cs | 4 +- .../Clients/Storage/ProcessClient.cs | 4 +- .../Clients/Storage/SignClient.cs | 82 +++++++++ src/Altinn.App.Core/Interface/IAppEvents.cs | 1 + .../Interface/IAppResources.cs | 1 + src/Altinn.App.Core/Interface/IApplication.cs | 1 + .../Interface/IAuthentication.cs | 1 + .../Interface/IAuthorization.cs | 1 + src/Altinn.App.Core/Interface/IDSF.cs | 1 + src/Altinn.App.Core/Interface/IData.cs | 1 + src/Altinn.App.Core/Interface/IER.cs | 1 + src/Altinn.App.Core/Interface/IEvents.cs | 1 + src/Altinn.App.Core/Interface/IInstance.cs | 1 + .../Interface/IInstanceEvent.cs | 1 + .../Interface/IPersonLookup.cs | 1 + .../Interface/IPersonRetriever.cs | 1 + src/Altinn.App.Core/Interface/IPrefill.cs | 1 + src/Altinn.App.Core/Interface/IProcess.cs | 1 + src/Altinn.App.Core/Interface/IProfile.cs | 1 + src/Altinn.App.Core/Interface/IRegister.cs | 11 +- src/Altinn.App.Core/Interface/ISecrets.cs | 1 + src/Altinn.App.Core/Interface/ITaskEvents.cs | 1 + .../Interface/IUserTokenProvider.cs | 1 + .../Internal/App/IAppEvents.cs | 25 +++ .../Internal/App/IAppResources.cs | 174 ++++++++++++++++++ .../Internal/App/IApplicationClient.cs | 17 ++ .../Internal/Auth/IAuthenticationClient.cs | 14 ++ .../Internal/Auth/IAuthorizationClient.cs | 38 ++++ .../Internal/Auth/IUserTokenProvider.cs | 18 ++ .../Internal/Data/IDataClient.cs | 154 ++++++++++++++++ .../Internal/Events/IEventsClient.cs | 15 ++ .../Events/KeyVaultSecretCodeProvider.cs | 6 +- .../LayoutEvaluatorStateInitializer.cs | 4 +- .../Internal/Instances/IInstanceClient.cs | 135 ++++++++++++++ .../Instances/IInstanceEventClient.cs | 20 ++ .../Internal/Pdf/PdfService.cs | 26 +-- .../Internal/Prefill/IPrefill.cs | 25 +++ .../AltinnExtensionProperties/AltinnAction.cs | 2 +- .../AltinnGatewayExtension.cs | 16 ++ .../AltinnProperties.cs | 30 --- .../AltinnTaskExtension.cs | 37 ++++ .../Process/Elements/ConfirmationTask.cs | 2 - .../Internal/Process/Elements/DataTask.cs | 2 - .../Process/Elements/ExtensionElements.cs | 10 +- .../Internal/Process/Elements/FeedbackTask.cs | 2 - .../Process/ExpressionsExclusiveGateway.cs | 6 +- .../Internal/Process/IProcessClient.cs | 21 +++ .../Internal/Process/ITaskEvents.cs | 34 ++++ .../Internal/Process/ProcessEngine.cs | 61 ++++-- .../Process/ProcessEventDispatcher.cs | 30 +-- .../Internal/Process/ProcessNavigator.cs | 2 +- .../Internal/Process/ProcessReader.cs | 9 +- .../Internal/Profile/IProfileClient.cs | 17 ++ .../Internal/Registers/IAltinnPartyClient.cs | 24 +++ .../Internal/Registers/IOrganizationClient.cs | 17 ++ .../Internal/Registers/IPersonClient.cs | 25 +++ .../Internal/Secrets/ISecretsClient.cs | 38 ++++ .../Internal/Sign/ISignClient.cs | 16 ++ .../Internal/Sign/SignatureContext.cs | 117 ++++++++++++ .../Models/Process/ProcessChangeResult.cs | 12 +- .../Models/UserAction/UserActionContext.cs | 16 ++ .../Controllers/FileScanControllerTests.cs | 14 +- ...nstancesController_ActiveInstancesTests.cs | 29 +-- .../InstancesController_CopyInstanceTests.cs | 21 ++- .../StatelessDataControllerTests.cs | 28 +-- .../StatelessPagesControllerTests.cs | 6 +- .../Controllers/TextsControllerTests.cs | 5 +- .../Controllers/ValidateControllerTests.cs | 11 +- .../ValidateControllerValidateDataTests.cs | 9 +- .../Mocks/AuthorizationMock.cs | 9 +- ...tanceMockSI.cs => InstanceClientMockSi.cs} | 12 +- .../Mocks/PepWithPDPAuthorizationMockSI.cs | 17 +- test/Altinn.App.Api.Tests/Program.cs | 8 +- .../TestResources/Altinn.App.Api.xml | 20 +- .../Altinn.App.Core.Tests.csproj | 3 + .../Features/Action/SigningUserActionTests.cs | 97 ++++++++++ .../Features/Action/TestData/appmetadata.json | 72 ++++++++ .../Action/TestData/signing-task-process.bpmn | 49 +++++ .../Features/Action/UserActionFactoryTests.cs | 74 ++++++++ .../Validators/ValidationAppSITests.cs | 7 +- .../Implementation/AppResourcesSITests.cs | 1 - .../Implementation/DefaultPageOrderTest.cs | 5 +- .../Implementation/DefaultTaskEventsTests.cs | 15 +- .../Implementation/PersonClientTests.cs | 2 +- .../Clients/{ => Storage}/DataClientTests.cs | 6 +- .../Clients/Storage/SignClientTests.cs | 139 ++++++++++++++ .../Clients/Storage/TestData/ExampleModel.cs | 5 +- .../Internal/Pdf/PdfServiceTests.cs | 12 +- .../ExpressionsExclusiveGatewayTests.cs | 4 +- .../Internal/Process/ProcessEngineTest.cs | 13 +- .../Process/ProcessEventDispatcherTests.cs | 76 ++++---- .../Internal/Process/ProcessNavigatorTests.cs | 4 +- .../Internal/Process/ProcessReaderTests.cs | 10 +- .../TestData/simple-gateway-default.bpmn | 15 +- .../simple-gateway-with-join-gateway.bpmn | 10 +- .../Process/TestUtils/ProcessTestUtils.cs | 13 +- .../FullTests/LayoutTestUtils.cs | 7 +- 147 files changed, 2261 insertions(+), 656 deletions(-) create mode 100644 src/Altinn.App.Core/Features/Action/NullUserAction.cs create mode 100644 src/Altinn.App.Core/Features/Action/SigningUserAction.cs create mode 100644 src/Altinn.App.Core/Features/Action/UserActionFactory.cs create mode 100644 src/Altinn.App.Core/Features/IUserAction.cs delete mode 100644 src/Altinn.App.Core/Implementation/PersonService.cs rename src/Altinn.App.Core/Infrastructure/Clients/Register/{RegisterClient.cs => AltinnPartyClient.cs} (86%) delete mode 100644 src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterDSFClient.cs create mode 100644 src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs create mode 100644 src/Altinn.App.Core/Internal/App/IAppEvents.cs create mode 100644 src/Altinn.App.Core/Internal/App/IAppResources.cs create mode 100644 src/Altinn.App.Core/Internal/App/IApplicationClient.cs create mode 100644 src/Altinn.App.Core/Internal/Auth/IAuthenticationClient.cs create mode 100644 src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs create mode 100644 src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs create mode 100644 src/Altinn.App.Core/Internal/Data/IDataClient.cs create mode 100644 src/Altinn.App.Core/Internal/Events/IEventsClient.cs create mode 100644 src/Altinn.App.Core/Internal/Instances/IInstanceClient.cs create mode 100644 src/Altinn.App.Core/Internal/Instances/IInstanceEventClient.cs create mode 100644 src/Altinn.App.Core/Internal/Prefill/IPrefill.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnGatewayExtension.cs delete mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnProperties.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs create mode 100644 src/Altinn.App.Core/Internal/Process/IProcessClient.cs create mode 100644 src/Altinn.App.Core/Internal/Process/ITaskEvents.cs create mode 100644 src/Altinn.App.Core/Internal/Profile/IProfileClient.cs create mode 100644 src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs create mode 100644 src/Altinn.App.Core/Internal/Registers/IOrganizationClient.cs create mode 100644 src/Altinn.App.Core/Internal/Registers/IPersonClient.cs create mode 100644 src/Altinn.App.Core/Internal/Secrets/ISecretsClient.cs create mode 100644 src/Altinn.App.Core/Internal/Sign/ISignClient.cs create mode 100644 src/Altinn.App.Core/Internal/Sign/SignatureContext.cs create mode 100644 src/Altinn.App.Core/Models/UserAction/UserActionContext.cs rename test/Altinn.App.Api.Tests/Mocks/{InstanceMockSI.cs => InstanceClientMockSi.cs} (98%) create mode 100644 test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Action/TestData/appmetadata.json create mode 100644 test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process.bpmn create mode 100644 test/Altinn.App.Core.Tests/Features/Action/UserActionFactoryTests.cs rename test/Altinn.App.Core.Tests/Infrastructure/Clients/{ => Storage}/DataClientTests.cs (99%) create mode 100644 test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs rename {src/Altinn.App.Core => test/Altinn.App.Core.Tests}/Infrastructure/Clients/Storage/TestData/ExampleModel.cs (64%) diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index d1b34d80b..c6df17536 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -21,7 +21,6 @@ - diff --git a/src/Altinn.App.Api/Controllers/AuthenticationController.cs b/src/Altinn.App.Api/Controllers/AuthenticationController.cs index 526ba7361..2749bde11 100644 --- a/src/Altinn.App.Api/Controllers/AuthenticationController.cs +++ b/src/Altinn.App.Api/Controllers/AuthenticationController.cs @@ -1,9 +1,7 @@ -using System.Threading.Tasks; using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -14,13 +12,13 @@ namespace Altinn.App.Api.Controllers /// public class AuthenticationController : ControllerBase { - private readonly IAuthentication _authenticationClient; + private readonly IAuthenticationClient _authenticationClient; private readonly GeneralSettings _settings; /// /// Initializes a new instance of the class /// - public AuthenticationController(IAuthentication authenticationClient, IOptions settings) + public AuthenticationController(IAuthenticationClient authenticationClient, IOptions settings) { _authenticationClient = authenticationClient; _settings = settings.Value; diff --git a/src/Altinn.App.Api/Controllers/AuthorizationController.cs b/src/Altinn.App.Api/Controllers/AuthorizationController.cs index 68c58001e..a63839160 100644 --- a/src/Altinn.App.Api/Controllers/AuthorizationController.cs +++ b/src/Altinn.App.Api/Controllers/AuthorizationController.cs @@ -1,10 +1,10 @@ -using System.Threading.Tasks; using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -15,7 +15,7 @@ namespace Altinn.App.Api.Controllers /// public class AuthorizationController : Controller { - private readonly IAuthorization _authorization; + private readonly IAuthorizationClient _authorization; private readonly UserHelper _userHelper; private readonly GeneralSettings _settings; @@ -23,12 +23,12 @@ public class AuthorizationController : Controller /// Initializes a new instance of the class /// public AuthorizationController( - IAuthorization authorization, - IProfile profileClient, - IRegister registerClient, + IAuthorizationClient authorization, + IProfileClient profileClient, + IAltinnPartyClient altinnPartyClientClient, IOptions settings) { - _userHelper = new UserHelper(profileClient, registerClient, settings); + _userHelper = new UserHelper(profileClient, altinnPartyClientClient, settings); _authorization = authorization; _settings = settings.Value; } diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index dc1aebf4c..208e6063b 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -7,9 +7,11 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Serialization; -using Altinn.App.Core.Interface; 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.AspNetCore.Authorization; @@ -28,9 +30,9 @@ namespace Altinn.App.Api.Controllers public class DataController : ControllerBase { private readonly ILogger _logger; - private readonly IData _dataClient; + private readonly IDataClient _dataClient; private readonly IDataProcessor _dataProcessor; - private readonly IInstance _instanceClient; + private readonly IInstanceClient _instanceClient; private readonly IInstantiationProcessor _instantiationProcessor; private readonly IAppModel _appModel; private readonly IAppResources _appResourcesService; @@ -53,9 +55,9 @@ public class DataController : ControllerBase /// A service with prefill related logic. public DataController( ILogger logger, - IInstance instanceClient, + IInstanceClient instanceClient, IInstantiationProcessor instantiationProcessor, - IData dataClient, + IDataClient dataClient, IDataProcessor dataProcessor, IAppModel appModel, IAppResources appResourcesService, diff --git a/src/Altinn.App.Api/Controllers/DataTagsController.cs b/src/Altinn.App.Api/Controllers/DataTagsController.cs index 83eb68636..6526da82a 100644 --- a/src/Altinn.App.Api/Controllers/DataTagsController.cs +++ b/src/Altinn.App.Api/Controllers/DataTagsController.cs @@ -1,17 +1,15 @@ -using System; -using System.Linq; using System.Net.Mime; using System.Text.RegularExpressions; -using System.Threading.Tasks; using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Models; using Altinn.App.Core.Constants; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Infrastructure.Clients; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Instances; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Altinn.App.Api.Controllers @@ -26,8 +24,8 @@ namespace Altinn.App.Api.Controllers [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data/{dataGuid:guid}/tags")] public class DataTagsController : ControllerBase { - private readonly IInstance _instanceClient; - private readonly IData _dataClient; + private readonly IInstanceClient _instanceClient; + private readonly IDataClient _dataClient; /// /// Initialize a new instance of with the given services. @@ -35,8 +33,8 @@ public class DataTagsController : ControllerBase /// A client that can be used to send instance requests to storage. /// A client that can be used to send data requests to storage. public DataTagsController( - IInstance instanceClient, - IData dataClient) + IInstanceClient instanceClient, + IDataClient dataClient) { _instanceClient = instanceClient; _dataClient = dataClient; diff --git a/src/Altinn.App.Api/Controllers/FileScanController.cs b/src/Altinn.App.Api/Controllers/FileScanController.cs index 805fd95f1..6b94fc7a8 100644 --- a/src/Altinn.App.Api/Controllers/FileScanController.cs +++ b/src/Altinn.App.Api/Controllers/FileScanController.cs @@ -1,11 +1,9 @@ -using System; -using System.Threading.Tasks; -using Altinn.App.Api.Models; -using Altinn.App.Core.Interface; +using Altinn.App.Api.Models; +using Altinn.App.Core.Infrastructure.Clients; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Altinn.App.Api.Controllers @@ -17,12 +15,12 @@ namespace Altinn.App.Api.Controllers [ApiController] public class FileScanController : ControllerBase { - private readonly IInstance _instanceClient; + private readonly IInstanceClient _instanceClient; /// /// Initialises a new instance of the class /// - public FileScanController(IInstance instanceClient) + public FileScanController(IInstanceClient instanceClient) { _instanceClient = instanceClient; } diff --git a/src/Altinn.App.Api/Controllers/HomeController.cs b/src/Altinn.App.Api/Controllers/HomeController.cs index ce9ce7a69..2670a3902 100644 --- a/src/Altinn.App.Api/Controllers/HomeController.cs +++ b/src/Altinn.App.Api/Controllers/HomeController.cs @@ -2,7 +2,6 @@ using System.Text.Json; using System.Web; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index b55f32465..70102d018 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -12,10 +12,15 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Serialization; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Prefill; using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.Validation; @@ -47,11 +52,11 @@ public class InstancesController : ControllerBase { private readonly ILogger _logger; - private readonly IInstance _instanceClient; - private readonly IData _dataClient; - private readonly IRegister _registerClient; - private readonly IEvents _eventsService; - private readonly IProfile _profileClientClient; + private readonly IInstanceClient _instanceClient; + private readonly IDataClient _dataClient; + private readonly IAltinnPartyClient _altinnPartyClientClient; + private readonly IEventsClient _eventsClient; + private readonly IProfileClient _profileClient; private readonly IAppMetadata _appMetadata; private readonly IAppModel _appModel; @@ -61,6 +66,7 @@ public class InstancesController : ControllerBase private readonly IPrefill _prefillService; private readonly AppSettings _appSettings; private readonly IProcessEngine _processEngine; + private readonly IOrganizationClient _orgClient; private const long RequestSizeLimit = 2000 * 1024 * 1024; @@ -69,34 +75,36 @@ public class InstancesController : ControllerBase /// public InstancesController( ILogger logger, - IRegister registerClient, - IInstance instanceClient, - IData dataClient, + IAltinnPartyClient altinnPartyClientClient, + IInstanceClient instanceClient, + IDataClient dataClient, IAppMetadata appMetadata, IAppModel appModel, IInstantiationProcessor instantiationProcessor, IInstantiationValidator instantiationValidator, IPDP pdp, - IEvents eventsService, + IEventsClient eventsClient, IOptions appSettings, IPrefill prefillService, - IProfile profileClient, - IProcessEngine processEngine) + IProfileClient profileClient, + IProcessEngine processEngine, + IOrganizationClient orgClient) { _logger = logger; _instanceClient = instanceClient; _dataClient = dataClient; _appMetadata = appMetadata; - _registerClient = registerClient; + _altinnPartyClientClient = altinnPartyClientClient; _appModel = appModel; _instantiationProcessor = instantiationProcessor; _instantiationValidator = instantiationValidator; _pdp = pdp; - _eventsService = eventsService; + _eventsClient = eventsClient; _appSettings = appSettings.Value; _prefillService = prefillService; - _profileClientClient = profileClient; + _profileClient = profileClient; _processEngine = processEngine; + _orgClient = orgClient; } /// @@ -739,7 +747,7 @@ public async Task>> GetActiveInstances([FromRo { if (lastChangedBy?.Length == 9) { - Organization? organization = await _registerClient.ER.GetOrganization(lastChangedBy); + Organization? organization = await _orgClient.GetOrganization(lastChangedBy); if (organization is not null && !string.IsNullOrEmpty(organization.Name)) { userAndOrgLookup.Add(lastChangedBy, organization.Name); @@ -747,7 +755,7 @@ public async Task>> GetActiveInstances([FromRo } else if (int.TryParse(lastChangedBy, out int lastChangedByInt)) { - UserProfile? user = await _profileClientClient.GetUserProfile(lastChangedByInt); + UserProfile? user = await _profileClient.GetUserProfile(lastChangedByInt); if (user is not null && user.Party is not null && !string.IsNullOrEmpty(user.Party.Name)) { userAndOrgLookup.Add(lastChangedBy, user.Party.Name); @@ -899,7 +907,7 @@ private async Task LookupParty(InstanceOwner instanceOwner) { try { - return await _registerClient.GetParty(int.Parse(instanceOwner.PartyId)); + return await _altinnPartyClientClient.GetParty(int.Parse(instanceOwner.PartyId)); } catch (Exception e) when (e is not ServiceException) { @@ -916,12 +924,12 @@ private async Task LookupParty(InstanceOwner instanceOwner) if (!string.IsNullOrEmpty(instanceOwner.PersonNumber)) { lookupNumber = "personNumber"; - return await _registerClient.LookupParty(new PartyLookup { Ssn = instanceOwner.PersonNumber }); + return await _altinnPartyClientClient.LookupParty(new PartyLookup { Ssn = instanceOwner.PersonNumber }); } else if (!string.IsNullOrEmpty(instanceOwner.OrganisationNumber)) { lookupNumber = "organisationNumber"; - return await _registerClient.LookupParty(new PartyLookup { OrgNo = instanceOwner.OrganisationNumber }); + return await _altinnPartyClientClient.LookupParty(new PartyLookup { OrgNo = instanceOwner.OrganisationNumber }); } else { @@ -1034,7 +1042,7 @@ private async Task RegisterEvent(string eventType, Instance instance) { try { - await _eventsService.AddEvent(eventType, instance); + await _eventsClient.AddEvent(eventType, instance); } catch (Exception exception) { diff --git a/src/Altinn.App.Api/Controllers/PagesController.cs b/src/Altinn.App.Api/Controllers/PagesController.cs index 08df0534a..c9181ad4e 100644 --- a/src/Altinn.App.Api/Controllers/PagesController.cs +++ b/src/Altinn.App.Api/Controllers/PagesController.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Altinn.App.Core.Features; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; diff --git a/src/Altinn.App.Api/Controllers/PartiesController.cs b/src/Altinn.App.Api/Controllers/PartiesController.cs index 5a81343ae..68a5de46c 100644 --- a/src/Altinn.App.Api/Controllers/PartiesController.cs +++ b/src/Altinn.App.Api/Controllers/PartiesController.cs @@ -1,8 +1,10 @@ #nullable enable using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Profile.Models; @@ -21,9 +23,9 @@ namespace Altinn.App.Api.Controllers [ApiController] public class PartiesController : ControllerBase { - private readonly IAuthorization _authorizationClient; + private readonly IAuthorizationClient _authorizationClient; private readonly UserHelper _userHelper; - private readonly IProfile _profileClient; + private readonly IProfileClient _profileClient; private readonly GeneralSettings _settings; private readonly IAppMetadata _appMetadata; @@ -31,14 +33,14 @@ public class PartiesController : ControllerBase /// Initializes a new instance of the class /// public PartiesController( - IAuthorization authorizationClient, - IProfile profileClient, - IRegister registerClient, + IAuthorizationClient authorizationClient, + IProfileClient profileClient, + IAltinnPartyClient altinnPartyClientClient, IOptions settings, IAppMetadata appMetadata) { _authorizationClient = authorizationClient; - _userHelper = new UserHelper(profileClient, registerClient, settings); + _userHelper = new UserHelper(profileClient, altinnPartyClientClient, settings); _profileClient = profileClient; _settings = settings.Value; _appMetadata = appMetadata; diff --git a/src/Altinn.App.Api/Controllers/PdfController.cs b/src/Altinn.App.Api/Controllers/PdfController.cs index bc9bb481a..62692483e 100644 --- a/src/Altinn.App.Api/Controllers/PdfController.cs +++ b/src/Altinn.App.Api/Controllers/PdfController.cs @@ -2,8 +2,10 @@ using System.Text.Json; using Altinn.App.Core.Features; -using Altinn.App.Core.Interface; +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.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; @@ -19,11 +21,11 @@ namespace Altinn.App.Api.Controllers [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data/{dataGuid:guid}/pdf")] public class PdfController : ControllerBase { - private readonly IInstance _instanceClient; + private readonly IInstanceClient _instanceClient; private readonly IPdfFormatter _pdfFormatter; private readonly IAppResources _resources; private readonly IAppModel _appModel; - private readonly IData _dataClient; + private readonly IDataClient _dataClient; /// /// Initializes a new instance of the class. @@ -34,11 +36,11 @@ public class PdfController : ControllerBase /// The app model service /// The data client public PdfController( - IInstance instanceClient, + IInstanceClient instanceClient, IPdfFormatter pdfFormatter, IAppResources resources, IAppModel appModel, - IData dataClient) + IDataClient dataClient) { _instanceClient = instanceClient; _pdfFormatter = pdfFormatter; diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 3bc0b8534..13e75d260 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -3,7 +3,8 @@ using Altinn.App.Api.Models; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; @@ -30,10 +31,10 @@ public class ProcessController : ControllerBase private const int MaxIterationsAllowed = 100; private readonly ILogger _logger; - private readonly IInstance _instanceClient; - private readonly IProcess _processService; + private readonly IInstanceClient _instanceClient; + private readonly IProcessClient _processClient; private readonly IValidation _validationService; - private readonly IAuthorization _authorization; + private readonly IAuthorizationClient _authorization; private readonly IProcessEngine _processEngine; private readonly IProcessReader _processReader; @@ -42,16 +43,16 @@ public class ProcessController : ControllerBase /// public ProcessController( ILogger logger, - IInstance instanceClient, - IProcess processService, + IInstanceClient instanceClient, + IProcessClient processClient, IValidation validationService, - IAuthorization authorization, + IAuthorizationClient authorization, IProcessReader processReader, IProcessEngine processEngine) { _logger = logger; _instanceClient = instanceClient; - _processService = processService; + _processClient = processClient; _validationService = validationService; _authorization = authorization; _processReader = processReader; @@ -290,7 +291,13 @@ public async Task> NextElement( var result = await _processEngine.Next(request); if (!result.Success) { - return Conflict(result.ErrorMessage); + switch (result.ErrorType) + { + case ProcessErrorType.Conflict: + return Conflict(result.ErrorMessage); + case ProcessErrorType.Internal: + return StatusCode(500, result.ErrorMessage); + } } AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, result.ProcessStateChange?.NewProcessState); @@ -385,7 +392,13 @@ public async Task> CompleteProcess( if (!result.Success) { - return Conflict(result.ErrorMessage); + switch (result.ErrorType) + { + case ProcessErrorType.Conflict: + return Conflict(result.ErrorMessage); + case ProcessErrorType.Internal: + return StatusCode(500, result.ErrorMessage); + } } currentTaskId = result.ProcessStateChange?.NewProcessState.CurrentTask.ElementId; @@ -422,7 +435,7 @@ public async Task GetProcessHistory( { try { - return Ok(await _processService.GetProcessHistory(instanceGuid.ToString(), instanceOwnerPartyId.ToString())); + return Ok(await _processClient.GetProcessHistory(instanceGuid.ToString(), instanceOwnerPartyId.ToString())); } catch (PlatformHttpException e) { @@ -444,7 +457,7 @@ private async Task ConvertAndAuthorizeActions(string org, strin if (flowElement is ProcessTask processTask) { appProcessState.CurrentTask.Actions = new Dictionary(); - foreach (AltinnAction action in processTask.ExtensionElements?.AltinnProperties?.AltinnActions ?? new List()) + foreach (AltinnAction action in processTask.ExtensionElements?.TaskExtension?.AltinnActions ?? new List()) { appProcessState.CurrentTask.Actions.Add(action.Id, await AuthorizeAction(action.Id, org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id)); } diff --git a/src/Altinn.App.Api/Controllers/ProfileController.cs b/src/Altinn.App.Api/Controllers/ProfileController.cs index b0e91b276..7fd381c59 100644 --- a/src/Altinn.App.Api/Controllers/ProfileController.cs +++ b/src/Altinn.App.Api/Controllers/ProfileController.cs @@ -1,11 +1,7 @@ -using System; -using System.Threading.Tasks; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Profile; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace Altinn.App.Api.Controllers { @@ -17,14 +13,14 @@ namespace Altinn.App.Api.Controllers [ApiController] public class ProfileController : Controller { - private readonly IProfile _profileClient; + private readonly IProfileClient _profileClient; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; /// /// Initializes a new instance of the class /// - public ProfileController(IProfile profileClient, IHttpContextAccessor httpContextAccessor, ILogger logger) + public ProfileController(IProfileClient profileClient, IHttpContextAccessor httpContextAccessor, ILogger logger) { _profileClient = profileClient; _httpContextAccessor = httpContextAccessor; diff --git a/src/Altinn.App.Api/Controllers/ResourceController.cs b/src/Altinn.App.Api/Controllers/ResourceController.cs index bbe7adb2a..083b0a1b9 100644 --- a/src/Altinn.App.Api/Controllers/ResourceController.cs +++ b/src/Altinn.App.Api/Controllers/ResourceController.cs @@ -1,10 +1,8 @@ #nullable enable -using System; using System.Globalization; -using System.IO; using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; diff --git a/src/Altinn.App.Api/Controllers/StatelessDataController.cs b/src/Altinn.App.Api/Controllers/StatelessDataController.cs index 07b25e28b..4c40200f5 100644 --- a/src/Altinn.App.Api/Controllers/StatelessDataController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessDataController.cs @@ -2,13 +2,14 @@ using System.Net; using Altinn.App.Api.Infrastructure.Filters; -using Altinn.App.Api.Mappers; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Serialization; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Internal.Registers; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Helpers; using Altinn.Common.PEP.Interfaces; @@ -34,7 +35,7 @@ public class StatelessDataController : ControllerBase private readonly IAppResources _appResourcesService; private readonly IDataProcessor _dataProcessor; private readonly IPrefill _prefillService; - private readonly IRegister _registerClient; + private readonly IAltinnPartyClient _altinnPartyClientClient; private readonly IPDP _pdp; private const long REQUEST_SIZE_LIMIT = 2000 * 1024 * 1024; @@ -52,7 +53,7 @@ public StatelessDataController( IAppResources appResourcesService, IDataProcessor dataProcessor, IPrefill prefillService, - IRegister registerClient, + IAltinnPartyClient altinnPartyClientClient, IPDP pdp) { _logger = logger; @@ -60,7 +61,7 @@ public StatelessDataController( _appResourcesService = appResourcesService; _dataProcessor = dataProcessor; _prefillService = prefillService; - _registerClient = registerClient; + _altinnPartyClientClient = altinnPartyClientClient; _pdp = pdp; } @@ -272,7 +273,7 @@ public async Task PostAnonymous([FromQuery] string dataType) return null; } - var partyFromUser = await _registerClient.GetParty(partyId.Value); + var partyFromUser = await _altinnPartyClientClient.GetParty(partyId.Value); if (partyFromUser is null) { return null; @@ -292,11 +293,11 @@ public async Task PostAnonymous([FromQuery] string dataType) var idPrefix = headerParts[0].ToLowerInvariant(); var party = idPrefix switch { - PartyPrefix => await _registerClient.GetParty(int.TryParse(id, out var partyId) ? partyId : 0), + PartyPrefix => await _altinnPartyClientClient.GetParty(int.TryParse(id, out var partyId) ? partyId : 0), // Frontend seems to only use partyId, not orgnr or ssn. - PersonPrefix => await _registerClient.LookupParty(new PartyLookup { Ssn = id }), - OrgPrefix => await _registerClient.LookupParty(new PartyLookup { OrgNo = id }), + PersonPrefix => await _altinnPartyClientClient.LookupParty(new PartyLookup { Ssn = id }), + OrgPrefix => await _altinnPartyClientClient.LookupParty(new PartyLookup { OrgNo = id }), _ => null, }; diff --git a/src/Altinn.App.Api/Controllers/StatelessPagesController.cs b/src/Altinn.App.Api/Controllers/StatelessPagesController.cs index c57e74a1d..eb9abc586 100644 --- a/src/Altinn.App.Api/Controllers/StatelessPagesController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessPagesController.cs @@ -1,7 +1,5 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Altinn.App.Core.Features; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Models; using Microsoft.AspNetCore.Authorization; diff --git a/src/Altinn.App.Api/Controllers/TextsController.cs b/src/Altinn.App.Api/Controllers/TextsController.cs index c2b2e3d4a..eae00f1d6 100644 --- a/src/Altinn.App.Api/Controllers/TextsController.cs +++ b/src/Altinn.App.Api/Controllers/TextsController.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Mvc; diff --git a/src/Altinn.App.Api/Controllers/ValidateController.cs b/src/Altinn.App.Api/Controllers/ValidateController.cs index f2383cb20..b6237b409 100644 --- a/src/Altinn.App.Api/Controllers/ValidateController.cs +++ b/src/Altinn.App.Api/Controllers/ValidateController.cs @@ -1,7 +1,8 @@ using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Infrastructure.Clients; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; @@ -16,7 +17,7 @@ namespace Altinn.App.Api.Controllers [ApiController] public class ValidateController : ControllerBase { - private readonly IInstance _instanceClient; + private readonly IInstanceClient _instanceClient; private readonly IAppMetadata _appMetadata; private readonly IValidation _validationService; @@ -24,7 +25,7 @@ public class ValidateController : ControllerBase /// Initialises a new instance of the class /// public ValidateController( - IInstance instanceClient, + IInstanceClient instanceClient, IValidation validationService, IAppMetadata appMetadata) { diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index dd192ae00..66c52689d 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs index b246fae4b..cbcae69c0 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs @@ -1,8 +1,9 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.EFormidling.Interface; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Common.EFormidlingClient; @@ -28,9 +29,9 @@ public class DefaultEFormidlingService : IEFormidlingService private readonly IEFormidlingClient? _eFormidlingClient; private readonly IEFormidlingMetadata? _eFormidlingMetadata; private readonly IAppMetadata _appMetadata; - private readonly IData _dataClient; + private readonly IDataClient _dataClient; private readonly IEFormidlingReceivers _eFormidlingReceivers; - private readonly IEvents _eventClient; + private readonly IEventsClient _eventClient; /// /// Initializes a new instance of the class. @@ -39,9 +40,9 @@ public DefaultEFormidlingService( ILogger logger, IHttpContextAccessor httpContextAccessor, IAppMetadata appMetadata, - IData dataClient, + IDataClient dataClient, IEFormidlingReceivers eFormidlingReceivers, - IEvents eventClient, + IEventsClient eventClient, IOptions? appSettings = null, IOptions? platformSettings = null, IEFormidlingClient? eFormidlingClient = null, diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs index 9027b1a0c..b9fcdcc9d 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingStatusCheckEventHandler.cs @@ -8,19 +8,15 @@ using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients.Maskinporten; using Altinn.App.Core.Infrastructure.Clients.Storage; -using Altinn.App.Core.Interface; using Altinn.App.Core.Models; using Altinn.Common.EFormidlingClient; using Altinn.Common.EFormidlingClient.Models; using Altinn.Platform.Storage.Interface.Models; -using AltinnCore.Authentication.Utils; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; -using System; using System.Net; using System.Net.Http.Headers; -using System.Runtime; using System.Security.Cryptography.X509Certificates; namespace Altinn.App.Core.EFormidling.Implementation diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index d94281dd8..8deff2af6 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Action; using Altinn.App.Core.Features.DataLists; using Altinn.App.Core.Features.DataProcessing; using Altinn.App.Core.Features.Options; @@ -15,14 +16,21 @@ using Altinn.App.Core.Infrastructure.Clients.Profile; using Altinn.App.Core.Infrastructure.Clients.Register; using Altinn.App.Core.Infrastructure.Clients.Storage; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Language; using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Internal.Prefill; using Altinn.App.Core.Internal.Process; +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.Models; using Altinn.Common.AccessTokenClient.Configuration; @@ -63,26 +71,24 @@ public static void AddPlatformServices(this IServiceCollection services, IConfig AddApplicationIdentifier(services); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); services.AddHttpClient(); - services.AddHttpClient(); - services.Decorate(); - services.AddHttpClient(); + services.AddHttpClient(); + services.Decorate(); + services.AddHttpClient(); services.AddHttpClient(); - services.AddHttpClient(); - services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); services.TryAddTransient(); services.TryAddTransient(); - services.TryAddTransient(); services.TryAddTransient(); } @@ -145,18 +151,19 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati services.Configure(configuration.GetSection(nameof(FrontEndSettings))); services.Configure(configuration.GetSection(nameof(PdfGeneratorSettings))); AddAppOptions(services); + AddActionServices(services); AddPdfServices(services); AddEventServices(services); AddProcessServices(services); if (!env.IsDevelopment()) { - services.TryAddSingleton(); + services.TryAddSingleton(); services.Configure(configuration.GetSection("kvSetting")); } else { - services.TryAddSingleton(); + services.TryAddSingleton(); } } @@ -228,5 +235,13 @@ private static void AddProcessServices(IServiceCollection services) services.AddTransient(); services.TryAddTransient(); } + + private static void AddActionServices(IServiceCollection services) + { + services.TryAddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddHttpClient(); + } } } diff --git a/src/Altinn.App.Core/Features/Action/NullUserAction.cs b/src/Altinn.App.Core/Features/Action/NullUserAction.cs new file mode 100644 index 000000000..96f22b589 --- /dev/null +++ b/src/Altinn.App.Core/Features/Action/NullUserAction.cs @@ -0,0 +1,18 @@ +using Altinn.App.Core.Models.UserAction; + +namespace Altinn.App.Core.Features.Action; + +/// +/// Null action handler for cases where there is no match on the requested +/// +public class NullUserAction: IUserAction +{ + /// + public string Id => "null"; + + /// + public Task HandleAction(UserActionContext context) + { + return Task.FromResult(true); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs new file mode 100644 index 000000000..8d7196d36 --- /dev/null +++ b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs @@ -0,0 +1,85 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Sign; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.UserAction; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; +using Signee = Altinn.App.Core.Internal.Sign.Signee; + +namespace Altinn.App.Core.Features.Action; + +/// +/// Class handling tasks that should happen when action signing is performed. +/// +public class SigningUserAction: IUserAction +{ + private readonly IProcessReader _processReader; + private readonly ILogger _logger; + private readonly IAppMetadata _appMetadata; + private readonly IProfileClient _profileClient; + private readonly ISignClient _signClient; + + /// + /// Initializes a new instance of the class + /// + /// The process reader + /// The logger + /// The application metadata service + public SigningUserAction(IProcessReader processReader, ILogger logger, IAppMetadata appMetadata, IProfileClient profileClient, ISignClient signClient) + { + _logger = logger; + _appMetadata = appMetadata; + _profileClient = profileClient; + _signClient = signClient; + _processReader = processReader; + } + + /// + public string Id => "sign"; + + /// + /// + public async Task HandleAction(UserActionContext context) + { + if (_processReader.GetFlowElement(context.Instance.Process.CurrentTask.ElementId) is ProcessTask currentTask) + { + _logger.LogInformation("Signing action handler invoked for instance {Id}. In task: {CurrentTaskId}", context.Instance.Id, currentTask.Id); + var dataTypes = currentTask.ExtensionElements?.TaskExtension?.DataTypesToSign ?? new(); + var connectedDataElements = GetDataElementSignatures(context.Instance.Data, dataTypes); + if (connectedDataElements.Count > 0) + { + SignatureContext signatureContext = new SignatureContext(new InstanceIdentifier(context.Instance), currentTask.ExtensionElements?.TaskExtension?.SignatureDataType, await GetSignee(context.UserId), connectedDataElements); + await _signClient.SignDataElements(signatureContext); + } + return true; + } + + return false; + } + + private List GetDataElementSignatures(List dataElements, List dataTypesToSign) + { + var connectedDataElements = new List(); + foreach (var dataType in dataTypesToSign) + { + connectedDataElements.AddRange(dataElements.Where(d => d.DataType.Equals(dataType, StringComparison.OrdinalIgnoreCase)).Select(d => new DataElementSignature(d.Id))); + } + + return connectedDataElements; + } + + private async Task GetSignee(int userId) + { + var userProfile = await _profileClient.GetUserProfile(userId); + return new Signee + { + UserId = userProfile.UserId.ToString(), + PersonNumber = userProfile.Party.SSN, + OrganisationNumber = userProfile.Party.OrgNumber, + }; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Action/UserActionFactory.cs b/src/Altinn.App.Core/Features/Action/UserActionFactory.cs new file mode 100644 index 000000000..636dcc3c0 --- /dev/null +++ b/src/Altinn.App.Core/Features/Action/UserActionFactory.cs @@ -0,0 +1,34 @@ +namespace Altinn.App.Core.Features.Action; + +/// +/// Factory class for resolving implementations +/// based on the id of the action. +/// +public class UserActionFactory +{ + private readonly IEnumerable _actionHandlers; + + /// + /// Initializes a new instance of the class. + /// + /// The list of action handlers to choose from. + public UserActionFactory(IEnumerable actionHandlers) + { + _actionHandlers = actionHandlers; + } + + /// + /// Find the implementation of based on the actionId + /// + /// The id of the action to handle. + /// The first implementation of that matches the actionId. If no match is returned + public IUserAction GetActionHandler(string? actionId) + { + if (actionId != null) + { + return _actionHandlers.Where(ah => ah.Id.Equals(actionId, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(new NullUserAction()); + } + + return new NullUserAction(); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/IUserAction.cs b/src/Altinn.App.Core/Features/IUserAction.cs new file mode 100644 index 000000000..2f1b5d652 --- /dev/null +++ b/src/Altinn.App.Core/Features/IUserAction.cs @@ -0,0 +1,10 @@ +using Altinn.App.Core.Models.UserAction; + +namespace Altinn.App.Core.Features; + +public interface IUserAction +{ + string Id { get; } + + Task HandleAction(UserActionContext context); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs b/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs index 43984971b..c3f868b6f 100644 --- a/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs +++ b/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; namespace Altinn.App.Core.Features.PageOrder diff --git a/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs b/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs index a61be9c38..7bffeb2b4 100644 --- a/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs +++ b/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs @@ -1,8 +1,9 @@ using Altinn.App.Core.Configuration; -using Altinn.App.Core.Interface; 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.Models.Validation; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; @@ -22,8 +23,8 @@ namespace Altinn.App.Core.Features.Validation public class ValidationAppSI : IValidation { private readonly ILogger _logger; - private readonly IData _dataService; - private readonly IInstance _instanceService; + private readonly IDataClient _dataClient; + private readonly IInstanceClient _instanceClient; private readonly IInstanceValidator _instanceValidator; private readonly IAppModel _appModel; private readonly IAppResources _appResourcesService; @@ -39,8 +40,8 @@ public class ValidationAppSI : IValidation /// public ValidationAppSI( ILogger logger, - IData dataService, - IInstance instanceService, + IDataClient dataClient, + IInstanceClient instanceClient, IInstanceValidator instanceValidator, IAppModel appModel, IAppResources appResourcesService, @@ -52,8 +53,8 @@ public ValidationAppSI( IOptions appSettings) { _logger = logger; - _dataService = dataService; - _instanceService = instanceService; + _dataClient = dataClient; + _instanceClient = instanceClient; _instanceValidator = instanceValidator; _appModel = appModel; _appResourcesService = appResourcesService; @@ -127,7 +128,7 @@ public async Task> ValidateAndUpdateProcess(Instance insta Timestamp = DateTime.Now }; - await _instanceService.UpdateProcess(instance); + await _instanceClient.UpdateProcess(instance); return messages; } @@ -223,7 +224,7 @@ public async Task> ValidateDataElement(Instance instance, Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); string app = instance.AppId.Split("/")[1]; int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); - object data = await _dataService.GetFormData( + object data = await _dataClient.GetFormData( instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, Guid.Parse(dataElement.Id)); if (_appSettings.RemoveHiddenDataPreview) diff --git a/src/Altinn.App.Core/Helpers/UserHelper.cs b/src/Altinn.App.Core/Helpers/UserHelper.cs index e7cfe7e2b..fdbe76921 100644 --- a/src/Altinn.App.Core/Helpers/UserHelper.cs +++ b/src/Altinn.App.Core/Helpers/UserHelper.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.Platform.Profile.Models; using AltinnCore.Authentication.Constants; @@ -14,20 +15,20 @@ namespace Altinn.App.Core.Helpers /// public class UserHelper { - private readonly IProfile _profileService; - private readonly IRegister _registerService; + private readonly IProfileClient _profileClient; + private readonly IAltinnPartyClient _altinnPartyClientService; private readonly GeneralSettings _settings; /// /// Initializes a new instance of the class /// - /// The ProfileService (defined in Startup.cs) - /// The RegisterService (defined in Startup.cs) + /// The ProfileService (defined in Startup.cs) + /// The RegisterService (defined in Startup.cs) /// The general settings - public UserHelper(IProfile profileService, IRegister registerService, IOptions settings) + public UserHelper(IProfileClient profileClient, IAltinnPartyClient altinnPartyClientService, IOptions settings) { - _profileService = profileService; - _registerService = registerService; + _profileClient = profileClient; + _altinnPartyClientService = altinnPartyClientService; _settings = settings.Value; } @@ -63,7 +64,7 @@ public async Task GetUserContext(HttpContext context) } } - UserProfile userProfile = await _profileService.GetUserProfile(userContext.UserId); + UserProfile userProfile = await _profileClient.GetUserProfile(userContext.UserId); userContext.UserParty = userProfile.Party; if (context.Request.Cookies[_settings.GetAltinnPartyCookieName] != null) @@ -77,7 +78,7 @@ public async Task GetUserContext(HttpContext context) } else { - userContext.Party = await _registerService.GetParty(userContext.PartyId); + userContext.Party = await _altinnPartyClientService.GetParty(userContext.PartyId); } return userContext; diff --git a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs index 57e0c8bc0..924f71915 100644 --- a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs +++ b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs @@ -2,7 +2,6 @@ using System.Text.Json; using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Layout; diff --git a/src/Altinn.App.Core/Implementation/DefaultAppEvents.cs b/src/Altinn.App.Core/Implementation/DefaultAppEvents.cs index 0c13c5f26..4406e7d7d 100644 --- a/src/Altinn.App.Core/Implementation/DefaultAppEvents.cs +++ b/src/Altinn.App.Core/Implementation/DefaultAppEvents.cs @@ -1,5 +1,6 @@ -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; @@ -13,8 +14,8 @@ public class DefaultAppEvents: IAppEvents { private readonly ILogger _logger; private readonly IAppMetadata _appMetadata; - private readonly IInstance _instanceClient; - private readonly IData _dataClient; + private readonly IInstanceClient _instanceClient; + private readonly IDataClient _dataClient; /// /// Constructor with services from DI @@ -22,8 +23,8 @@ public class DefaultAppEvents: IAppEvents public DefaultAppEvents( ILogger logger, IAppMetadata appMetadata, - IInstance instanceClient, - IData dataClient) + IInstanceClient instanceClient, + IDataClient dataClient) { _logger = logger; _appMetadata = appMetadata; diff --git a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs index cc55b5c4f..75cb9ee81 100644 --- a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs +++ b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs @@ -5,11 +5,14 @@ using Altinn.App.Core.EFormidling.Interface; using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; 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.Models; using Microsoft.Extensions.Logging; @@ -26,11 +29,11 @@ public class DefaultTaskEvents : ITaskEvents private readonly ILogger _logger; private readonly IAppResources _appResources; private readonly IAppMetadata _appMetadata; - private readonly IData _dataClient; + private readonly IDataClient _dataClient; private readonly IPrefill _prefillService; private readonly IAppModel _appModel; private readonly IInstantiationProcessor _instantiationProcessor; - private readonly IInstance _instanceClient; + private readonly IInstanceClient _instanceClient; private readonly IEnumerable _taskStarts; private readonly IEnumerable _taskEnds; private readonly IEnumerable _taskAbandons; @@ -47,11 +50,11 @@ public DefaultTaskEvents( ILogger logger, IAppResources appResources, IAppMetadata appMetadata, - IData dataClient, + IDataClient dataClient, IPrefill prefillService, IAppModel appModel, IInstantiationProcessor instantiationProcessor, - IInstance instanceClient, + IInstanceClient instanceClient, IEnumerable taskStarts, IEnumerable taskEnds, IEnumerable taskAbandons, diff --git a/src/Altinn.App.Core/Implementation/PersonService.cs b/src/Altinn.App.Core/Implementation/PersonService.cs deleted file mode 100644 index 06a260956..000000000 --- a/src/Altinn.App.Core/Implementation/PersonService.cs +++ /dev/null @@ -1,30 +0,0 @@ -#nullable enable - -using Altinn.App.Core.Interface; -using Altinn.Platform.Register.Models; - -namespace Altinn.App.Core.Implementation -{ - /// - /// Represents a collection of business logic operation related to persons. - /// - public class PersonService : IPersonLookup - { - private readonly IPersonRetriever _personRetriever; - - /// - /// Initializes a new instance of the class. - /// - /// An implementation of able to obtain a . - public PersonService(IPersonRetriever personRetriever) - { - _personRetriever = personRetriever; - } - - /// - public Task GetPerson(string nationalIdentityNumber, string lastName, CancellationToken ct) - { - return _personRetriever.GetPerson(nationalIdentityNumber, lastName, ct); - } - } -} diff --git a/src/Altinn.App.Core/Implementation/PrefillSI.cs b/src/Altinn.App.Core/Implementation/PrefillSI.cs index 086a81e8a..b604a1b59 100644 --- a/src/Altinn.App.Core/Implementation/PrefillSI.cs +++ b/src/Altinn.App.Core/Implementation/PrefillSI.cs @@ -1,6 +1,9 @@ using System.Reflection; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; @@ -15,9 +18,9 @@ namespace Altinn.App.Core.Implementation public class PrefillSI : IPrefill { private readonly ILogger _logger; - private readonly IProfile _profileClient; + private readonly IProfileClient _profileClient; private readonly IAppResources _appResourcesService; - private readonly IRegister _registerClient; + private readonly IAltinnPartyClient _altinnPartyClientClient; private readonly IHttpContextAccessor _httpContextAccessor; private static readonly string ER_KEY = "ER"; private static readonly string DSF_KEY = "DSF"; @@ -31,19 +34,19 @@ public class PrefillSI : IPrefill /// The logger /// The profile client /// The app's resource service - /// The register client + /// The register client /// A service with access to the http context. public PrefillSI( ILogger logger, - IProfile profileClient, + IProfileClient profileClient, IAppResources appResourcesService, - IRegister registerClient, + IAltinnPartyClient altinnPartyClientClient, IHttpContextAccessor httpContextAccessor) { _logger = logger; _profileClient = profileClient; _appResourcesService = appResourcesService; - _registerClient = registerClient; + _altinnPartyClientClient = altinnPartyClientClient; _httpContextAccessor = httpContextAccessor; } @@ -75,7 +78,7 @@ public async Task PrefillDataModel(string partyId, string dataModelName, object allowOverwrite = allowOverwriteToken.ToObject(); } - Party party = await _registerClient.GetParty(int.Parse(partyId)); + Party party = await _altinnPartyClientClient.GetParty(int.Parse(partyId)); if (party == null) { string errorMessage = $"Could find party for partyId: {partyId}"; diff --git a/src/Altinn.App.Core/Implementation/UserTokenProvider.cs b/src/Altinn.App.Core/Implementation/UserTokenProvider.cs index eca3d4a1f..24dd81dae 100644 --- a/src/Altinn.App.Core/Implementation/UserTokenProvider.cs +++ b/src/Altinn.App.Core/Implementation/UserTokenProvider.cs @@ -2,7 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; using AltinnCore.Authentication.Utils; using Microsoft.AspNetCore.Http; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs index caf3f83f9..52938a3c5 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs @@ -2,7 +2,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; using AltinnCore.Authentication.Utils; using Microsoft.AspNetCore.Http; @@ -14,7 +14,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Authentication /// /// A client for authentication actions in Altinn Platform. /// - public class AuthenticationClient : IAuthentication + public class AuthenticationClient : IAuthenticationClient { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs index 9d3146a16..087c6d1b0 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs @@ -3,7 +3,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Models; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Helpers; @@ -23,7 +23,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Authorization /// /// Client for handling authorization actions in Altinn Platform. /// - public class AuthorizationClient : IAuthorization + public class AuthorizationClient : IAuthorizationClient { private readonly IHttpContextAccessor _httpContextAccessor; private readonly AppSettings _settings; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs index fbb9ced79..f67585a14 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs @@ -5,8 +5,8 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Storage.Interface.Models; @@ -19,9 +19,8 @@ namespace Altinn.App.Core.Infrastructure.Clients.Events /// /// A client for handling actions on events in Altinn Platform. /// - public class EventsClient : IEvents + public class EventsClient : IEventsClient { - private readonly PlatformSettings _platformSettings; private readonly IHttpContextAccessor _httpContextAccessor; private readonly AppSettings _settings; private readonly GeneralSettings _generalSettings; @@ -48,7 +47,6 @@ public EventsClient( IOptionsMonitor settings, IOptions generalSettings) { - _platformSettings = platformSettings.Value; _httpContextAccessor = httpContextAccessor; _settings = settings.CurrentValue; _generalSettings = generalSettings.Value; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsClient.cs index 755bae58d..e18d37d78 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsClient.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Secrets; using AltinnCore.Authentication.Constants; using Microsoft.Azure.KeyVault; @@ -12,7 +12,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.KeyVault /// /// Class that handles integration with Azure Key Vault /// - public class SecretsClient : ISecrets + public class SecretsClient : ISecretsClient { private readonly string _vaultUri; private readonly AzureServiceTokenProvider _azureServiceTokenProvider; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs index 074dc2691..65170b2f7 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs @@ -1,5 +1,5 @@ using System.Text.Json; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Secrets; using Microsoft.Azure.KeyVault; using Microsoft.Azure.KeyVault.WebKey; using Microsoft.Extensions.Configuration; @@ -10,7 +10,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.KeyVault /// /// Class that handles integration with Azure Key Vault /// - public class SecretsLocalClient : ISecrets + public class SecretsLocalClient : ISecretsClient { private readonly IConfiguration _configuration; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs index c650f548d..1033ebade 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs @@ -3,11 +3,10 @@ using Altinn.App.Core.Internal.Pdf; -using Altinn.App.Core.Interface; - using Microsoft.Extensions.Options; using Altinn.App.Core.Models.Pdf; using Altinn.App.Core.Configuration; +using Altinn.App.Core.Internal.Auth; namespace Altinn.App.Core.Infrastructure.Clients.Pdf; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs index c667b6112..a0eb7ea1f 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs @@ -2,8 +2,8 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Profile.Models; @@ -17,7 +17,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Profile /// /// A client for retrieving profiles from Altinn Platform. /// - public class ProfileClient : IProfile + public class ProfileClient : IProfileClient { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs index 199952b3a..70dcd07b2 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs @@ -1,5 +1,5 @@ using Altinn.App.Core.Configuration; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Profile; using Altinn.Platform.Profile.Models; using Microsoft.Extensions.Caching.Memory; @@ -8,19 +8,19 @@ namespace Altinn.App.Core.Infrastructure.Clients.Profile { /// . - /// Decorates an implementation of IProfile by caching the party object. + /// Decorates an implementation of IProfileClient by caching the party object. /// If available, object is retrieved from cache without calling the service /// - public class ProfileClientCachingDecorator : IProfile + public class ProfileClientCachingDecorator : IProfileClient { - private readonly IProfile _decoratedService; + private readonly IProfileClient _decoratedService; private readonly IMemoryCache _memoryCache; private readonly MemoryCacheEntryOptions _cacheOptions; /// /// Initializes a new instance of the class. /// - public ProfileClientCachingDecorator(IProfile decoratedService, IMemoryCache memoryCache, IOptions _settings) + public ProfileClientCachingDecorator(IProfileClient decoratedService, IMemoryCache memoryCache, IOptions _settings) { _decoratedService = decoratedService; _memoryCache = memoryCache; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs similarity index 86% rename from src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterClient.cs rename to src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs index becbd326e..f5a03058d 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs @@ -4,8 +4,8 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Register.Models; @@ -20,10 +20,8 @@ namespace Altinn.App.Core.Infrastructure.Clients.Register /// /// A client for retrieving register data from Altinn Platform. /// - public class RegisterClient : IRegister + public class AltinnPartyClient : IAltinnPartyClient { - private readonly IDSF _dsfClient; - private readonly IER _erClient; private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; private readonly AppSettings _settings; @@ -32,30 +30,27 @@ public class RegisterClient : IRegister private readonly IAccessTokenGenerator _accessTokenGenerator; /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class /// /// The current platform settings. /// The dsf - /// The er + /// The organizationClient /// The logger /// The http context accessor /// The application settings. /// The http client /// The app metadata service /// The platform access token generator - public RegisterClient( + public AltinnPartyClient( IOptions platformSettings, - IDSF dsf, - IER er, - ILogger logger, + IOrganizationClient organizationClient, + ILogger logger, IHttpContextAccessor httpContextAccessor, IOptionsMonitor settings, HttpClient httpClient, IAppMetadata appMetadata, IAccessTokenGenerator accessTokenGenerator) { - _dsfClient = dsf; - _erClient = er; _logger = logger; _httpContextAccessor = httpContextAccessor; _settings = settings.CurrentValue; @@ -67,22 +62,6 @@ public RegisterClient( _accessTokenGenerator = accessTokenGenerator; } - /// - /// The access to the dsf component through register services - /// - public IDSF DSF - { - get { return _dsfClient; } - } - - /// - /// The access to the er component through register services - /// - public IER ER - { - get { return _erClient; } - } - /// public async Task GetParty(int partyId) { diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs index 63bed1f14..bd8317546 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/PersonClient.cs @@ -8,8 +8,9 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Register.Models; @@ -18,10 +19,10 @@ namespace Altinn.App.Core.Infrastructure.Clients.Register { /// - /// Represents an implementation of that will call the Register + /// Represents an implementation of that will call the Register /// component to retrieve person information. /// - public class PersonClient : IPersonRetriever + public class PersonClient : IPersonClient { private readonly HttpClient _httpClient; private readonly IAppMetadata _appMetadata; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterDSFClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterDSFClient.cs deleted file mode 100644 index f715969e9..000000000 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterDSFClient.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Net.Http.Headers; -using Altinn.App.Core.Configuration; -using Altinn.App.Core.Constants; -using Altinn.App.Core.Extensions; -using Altinn.App.Core.Interface; -using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Models; -using Altinn.Common.AccessTokenClient.Services; -using Altinn.Platform.Register.Models; -using AltinnCore.Authentication.Utils; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Altinn.App.Core.Infrastructure.Clients.Register -{ - /// - /// A client for retriecing DSF data from Altinn Platform. - /// - public class RegisterDSFClient : IDSF - { - private readonly ILogger _logger; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly AppSettings _settings; - private readonly HttpClient _client; - private readonly IAccessTokenGenerator _accessTokenGenerator; - private readonly IAppMetadata _appMetadata; - - /// - /// Initializes a new instance of the class - /// - /// The platform settings from loaded configuration. - /// the logger - /// The http context accessor - /// The application settings. - /// The http client - /// The platform access token generator - /// The app metadata service - public RegisterDSFClient( - IOptions platformSettings, - ILogger logger, - IHttpContextAccessor httpContextAccessor, - IOptionsMonitor settings, - HttpClient httpClient, - IAccessTokenGenerator accessTokenGenerator, - IAppMetadata appMetadata) - { - _logger = logger; - _httpContextAccessor = httpContextAccessor; - _settings = settings.CurrentValue; - httpClient.BaseAddress = new Uri(platformSettings.Value.ApiRegisterEndpoint); - httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - _client = httpClient; - _accessTokenGenerator = accessTokenGenerator; - _appMetadata = appMetadata; - } - - /// - public async Task GetPerson(string SSN) - { - Person? person = null; - - string endpointUrl = $"persons/{SSN}"; - - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); - - ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); - HttpResponseMessage response = await _client.GetAsync(token, endpointUrl, _accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App)); - if (response.StatusCode == System.Net.HttpStatusCode.OK) - { - person = await response.Content.ReadAsAsync(); - } - else - { - _logger.LogError("Getting person with ssn {Ssn} failed with statuscode {StatusCode}", SSN, response.StatusCode); - } - - return person; - } - } -} diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs index 6a6e9c0fd..0b21614b0 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs @@ -2,8 +2,8 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Register.Models; @@ -17,7 +17,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Register /// /// A client for retrieving ER data from Altinn Platform. /// - public class RegisterERClient : IER + public class RegisterERClient : IOrganizationClient { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ApplicationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ApplicationClient.cs index 173c7fd12..e1c8dd9c1 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ApplicationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ApplicationClient.cs @@ -2,7 +2,7 @@ using System.Net.Http.Headers; using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; @@ -15,7 +15,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage /// /// Client for retrieving application for Altinn Platform /// - public class ApplicationClient : IApplication + public class ApplicationClient : IApplicationClient { private readonly ILogger _logger; private readonly HttpClient _client; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index a5eef17d2..c1c834e57 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -8,12 +8,9 @@ using Altinn.App.Core.Helpers.Serialization; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; -using AltinnCore.Authentication.Utils; - using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -21,14 +18,15 @@ using Newtonsoft.Json; using System.Xml; -using Microsoft.IdentityModel.Tokens; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Data; namespace Altinn.App.Core.Infrastructure.Clients.Storage { /// /// A client for handling actions on data in Altinn Platform. /// - public class DataClient : IData + public class DataClient : IDataClient { private readonly PlatformSettings _platformSettings; private readonly ILogger _logger; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs index 3844531e3..9c57d0ccf 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClient.cs @@ -5,7 +5,7 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; @@ -23,7 +23,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage /// /// A client for handling actions on instances in Altinn Platform. /// - public class InstanceClient : IInstance + public class InstanceClient : IInstanceClient { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs index b13f07e88..8d0a243db 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceEventClient.cs @@ -4,7 +4,7 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Instances; using Altinn.Platform.Storage.Interface.Models; using AltinnCore.Authentication.Utils; @@ -18,7 +18,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage /// /// A client for handling actions on instance events in Altinn Platform. /// - public class InstanceEventClient : IInstanceEvent + public class InstanceEventClient : IInstanceEventClient { private readonly IHttpContextAccessor _httpContextAccessor; private readonly AppSettings _settings; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs index df905700f..a6dd3cde3 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs @@ -3,7 +3,7 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Process; using Altinn.Platform.Storage.Interface.Models; using AltinnCore.Authentication.Utils; using Microsoft.AspNetCore.Http; @@ -16,7 +16,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage /// /// The app implementation of the process service. /// - public class ProcessClient : IProcess + public class ProcessClient : IProcessClient { private readonly AppSettings _appSettings; private readonly ILogger _logger; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs new file mode 100644 index 000000000..227a04833 --- /dev/null +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs @@ -0,0 +1,82 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Constants; +using Altinn.App.Core.Extensions; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Sign; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Infrastructure.Clients.Storage +{ + /// + /// Implementation of that sends signing requests to platform + /// + public class SignClient: ISignClient + { + private readonly IUserTokenProvider _userTokenProvider; + private readonly HttpClient _client; + + /// + /// Create a new instance of + /// + /// Platform settings, used to get storage endpoint + /// HttpClient used to send requests + /// Service that can provide user token + public SignClient( + IOptions platformSettings, + HttpClient httpClient, + IUserTokenProvider userTokenProvider) + { + var platformSettings1 = platformSettings.Value; + + httpClient.BaseAddress = new Uri(platformSettings1.ApiStorageEndpoint); + httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings1.SubscriptionKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + _client = httpClient; + _userTokenProvider = userTokenProvider; + } + + /// + public async Task SignDataElements(SignatureContext signatureContext) + { + string apiUrl = $"instances/{signatureContext.InstanceIdentifier}/sign"; + string token = _userTokenProvider.GetUserToken(); + HttpResponseMessage response = await _client.PostAsync(token, apiUrl, BuildSignRequest(signatureContext)); + if (response.IsSuccessStatusCode) + { + return; + } + + throw new PlatformHttpException(response, "Failed to sign dataelements"); + } + + private static JsonContent BuildSignRequest(SignatureContext signatureContext) + { + SignRequest signRequest = new SignRequest() + { + Signee = new() + { + UserId = signatureContext.Signee.UserId, + PersonNumber = signatureContext.Signee.PersonNumber, + OrganisationNumber = signatureContext.Signee.OrganisationNumber + }, + SignatureDocumentDataType = signatureContext.SignatureDataTypeId, + DataElementSignatures = new() + }; + foreach (var dataElementSignature in signatureContext.DataElementSignatures) + { + signRequest.DataElementSignatures.Add(new SignRequest.DataElementSignature() + { + DataElementId = dataElementSignature.DataElementId, + Signed = dataElementSignature.Signed + }); + } + + return JsonContent.Create(signRequest); + } + } +} diff --git a/src/Altinn.App.Core/Interface/IAppEvents.cs b/src/Altinn.App.Core/Interface/IAppEvents.cs index 6c4d04dd5..9db8e3186 100644 --- a/src/Altinn.App.Core/Interface/IAppEvents.cs +++ b/src/Altinn.App.Core/Interface/IAppEvents.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface; /// /// Interface for implementing a receiver handling instance events. /// +[Obsolete(message: "Use Altinn.App.Core.Internal.App.IAppEvents instead", error: true)] public interface IAppEvents { /// diff --git a/src/Altinn.App.Core/Interface/IAppResources.cs b/src/Altinn.App.Core/Interface/IAppResources.cs index b4e471e69..1b2978d24 100644 --- a/src/Altinn.App.Core/Interface/IAppResources.cs +++ b/src/Altinn.App.Core/Interface/IAppResources.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for execution functionality /// + [Obsolete(message: "Use Altinn.App.Core.Internal.App.IAppResources instead", error: true)] public interface IAppResources { /// diff --git a/src/Altinn.App.Core/Interface/IApplication.cs b/src/Altinn.App.Core/Interface/IApplication.cs index 64ae78f4e..b2caf3d88 100644 --- a/src/Altinn.App.Core/Interface/IApplication.cs +++ b/src/Altinn.App.Core/Interface/IApplication.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for retrieving application metadata data related operations /// + [Obsolete(message: "Use Altinn.App.Core.Internal.App.IApplicationClient instead", error: true)] public interface IApplication { /// diff --git a/src/Altinn.App.Core/Interface/IAuthentication.cs b/src/Altinn.App.Core/Interface/IAuthentication.cs index b81b2b707..e0c40ae5b 100644 --- a/src/Altinn.App.Core/Interface/IAuthentication.cs +++ b/src/Altinn.App.Core/Interface/IAuthentication.cs @@ -3,6 +3,7 @@ namespace Altinn.App.Core.Interface /// /// Authentication interface. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Auth.IAuthenticationClient instead", error: true)] public interface IAuthentication { /// diff --git a/src/Altinn.App.Core/Interface/IAuthorization.cs b/src/Altinn.App.Core/Interface/IAuthorization.cs index 64dd95d89..a8ec8ef1c 100644 --- a/src/Altinn.App.Core/Interface/IAuthorization.cs +++ b/src/Altinn.App.Core/Interface/IAuthorization.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for authorization functionality. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Auth.IAuthorizationClient instead", error: true)] public interface IAuthorization { /// diff --git a/src/Altinn.App.Core/Interface/IDSF.cs b/src/Altinn.App.Core/Interface/IDSF.cs index 4d34dd5fb..dc8204825 100644 --- a/src/Altinn.App.Core/Interface/IDSF.cs +++ b/src/Altinn.App.Core/Interface/IDSF.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for the resident registration database (DSF: Det sentrale folkeregisteret) /// + [Obsolete(message: "Upstream API changed. Use Altinn.App.Core.Internal.Registers.IPersonClient instead", error: true)] public interface IDSF { /// diff --git a/src/Altinn.App.Core/Interface/IData.cs b/src/Altinn.App.Core/Interface/IData.cs index 643dfd037..742c5f6b2 100644 --- a/src/Altinn.App.Core/Interface/IData.cs +++ b/src/Altinn.App.Core/Interface/IData.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for data handling /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Data.IDataClient instead", error: true)] public interface IData { /// diff --git a/src/Altinn.App.Core/Interface/IER.cs b/src/Altinn.App.Core/Interface/IER.cs index a95e0c99c..18f497dfa 100644 --- a/src/Altinn.App.Core/Interface/IER.cs +++ b/src/Altinn.App.Core/Interface/IER.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for the entity registry (ER: Enhetsregisteret) /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IOrganizationClient instead", error: true)] public interface IER { /// diff --git a/src/Altinn.App.Core/Interface/IEvents.cs b/src/Altinn.App.Core/Interface/IEvents.cs index c635e2e38..5ed3a1ccf 100644 --- a/src/Altinn.App.Core/Interface/IEvents.cs +++ b/src/Altinn.App.Core/Interface/IEvents.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Interface describing client implementations for the Events component in the Altinn 3 platform. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Events.IEventsClient instead", error: true)] public interface IEvents { /// diff --git a/src/Altinn.App.Core/Interface/IInstance.cs b/src/Altinn.App.Core/Interface/IInstance.cs index 875ff9a1d..46b5245b4 100644 --- a/src/Altinn.App.Core/Interface/IInstance.cs +++ b/src/Altinn.App.Core/Interface/IInstance.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for handling form data related operations /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Instances.IInstanceClient instead", error: true)] public interface IInstance { /// diff --git a/src/Altinn.App.Core/Interface/IInstanceEvent.cs b/src/Altinn.App.Core/Interface/IInstanceEvent.cs index 60a1bebb3..9669aaab8 100644 --- a/src/Altinn.App.Core/Interface/IInstanceEvent.cs +++ b/src/Altinn.App.Core/Interface/IInstanceEvent.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for handling instance event related operations /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Instances.IInstanceEventClient instead", error: true)] public interface IInstanceEvent { /// diff --git a/src/Altinn.App.Core/Interface/IPersonLookup.cs b/src/Altinn.App.Core/Interface/IPersonLookup.cs index 982d528cd..bfbb867d1 100644 --- a/src/Altinn.App.Core/Interface/IPersonLookup.cs +++ b/src/Altinn.App.Core/Interface/IPersonLookup.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Describes the methods required by a person lookup service. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IPersonClient instead", error: true)] public interface IPersonLookup { /// diff --git a/src/Altinn.App.Core/Interface/IPersonRetriever.cs b/src/Altinn.App.Core/Interface/IPersonRetriever.cs index 209d603d8..6c99166ec 100644 --- a/src/Altinn.App.Core/Interface/IPersonRetriever.cs +++ b/src/Altinn.App.Core/Interface/IPersonRetriever.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Describes the required methods for an implementation of a person repository client. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Register.IPersonClient instead", error: true)] public interface IPersonRetriever { /// diff --git a/src/Altinn.App.Core/Interface/IPrefill.cs b/src/Altinn.App.Core/Interface/IPrefill.cs index af36409d5..cdd9c9153 100644 --- a/src/Altinn.App.Core/Interface/IPrefill.cs +++ b/src/Altinn.App.Core/Interface/IPrefill.cs @@ -3,6 +3,7 @@ namespace Altinn.App.Core.Interface /// /// The prefill service /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Prefill.IPrefill instead", error: true)] public interface IPrefill { /// diff --git a/src/Altinn.App.Core/Interface/IProcess.cs b/src/Altinn.App.Core/Interface/IProcess.cs index 582fa4ef4..44288ef87 100644 --- a/src/Altinn.App.Core/Interface/IProcess.cs +++ b/src/Altinn.App.Core/Interface/IProcess.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Process service that encapsulate reading of the BPMN process definition. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Process.IProcessClient instead", error: true)] public interface IProcess { /// diff --git a/src/Altinn.App.Core/Interface/IProfile.cs b/src/Altinn.App.Core/Interface/IProfile.cs index 65c0e6054..2c0db8a98 100644 --- a/src/Altinn.App.Core/Interface/IProfile.cs +++ b/src/Altinn.App.Core/Interface/IProfile.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for profile functionality /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Profile.IProfileClient instead", error: true)] public interface IProfile { /// diff --git a/src/Altinn.App.Core/Interface/IRegister.cs b/src/Altinn.App.Core/Interface/IRegister.cs index 9856fb155..edfc92f97 100644 --- a/src/Altinn.App.Core/Interface/IRegister.cs +++ b/src/Altinn.App.Core/Interface/IRegister.cs @@ -5,18 +5,9 @@ namespace Altinn.App.Core.Interface /// /// Interface for register functionality /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IAltinnPartyClient instead", error: true)] public interface IRegister { - /// - /// The access to dsf methods through register - /// - IDSF DSF { get; } - - /// - /// The access to er methods through register - /// - IER ER { get; } - /// /// Returns party information /// diff --git a/src/Altinn.App.Core/Interface/ISecrets.cs b/src/Altinn.App.Core/Interface/ISecrets.cs index f717a0433..14e8a2ccd 100644 --- a/src/Altinn.App.Core/Interface/ISecrets.cs +++ b/src/Altinn.App.Core/Interface/ISecrets.cs @@ -6,6 +6,7 @@ namespace Altinn.App.Core.Interface /// /// Interface for secrets service /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Secrets.ISecretsClient instead", error: true)] public interface ISecrets { /// diff --git a/src/Altinn.App.Core/Interface/ITaskEvents.cs b/src/Altinn.App.Core/Interface/ITaskEvents.cs index 928fb87ea..0af8bb45a 100644 --- a/src/Altinn.App.Core/Interface/ITaskEvents.cs +++ b/src/Altinn.App.Core/Interface/ITaskEvents.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Interface; /// /// Interface for implementing a receiver handling task process events. /// +[Obsolete(message: "Use Altinn.App.Core.Internal.Process.ITaskEvents instead", error: true)] public interface ITaskEvents { /// diff --git a/src/Altinn.App.Core/Interface/IUserTokenProvider.cs b/src/Altinn.App.Core/Interface/IUserTokenProvider.cs index ea8a8db5c..86e30ec29 100644 --- a/src/Altinn.App.Core/Interface/IUserTokenProvider.cs +++ b/src/Altinn.App.Core/Interface/IUserTokenProvider.cs @@ -7,6 +7,7 @@ namespace Altinn.App.Core.Interface /// The provider is used by client implementations that needs the user token in requests /// against other systems. /// + [Obsolete(message: "Use Altinn.App.Core.Internal.Auth.IUserTokenProvider instead", error: true)] public interface IUserTokenProvider { /// diff --git a/src/Altinn.App.Core/Internal/App/IAppEvents.cs b/src/Altinn.App.Core/Internal/App/IAppEvents.cs new file mode 100644 index 000000000..4ca2cb84b --- /dev/null +++ b/src/Altinn.App.Core/Internal/App/IAppEvents.cs @@ -0,0 +1,25 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.App; + +/// +/// Interface for implementing a receiver handling instance events. +/// +public interface IAppEvents +{ + /// + /// Callback on first start event of process. + /// + /// Start event to start + /// Instance data + /// + public Task OnStartAppEvent(string startEvent, Instance instance); + + /// + /// Is called when the process for an instance is ended. + /// + /// End event to end + /// Instance data + /// + public Task OnEndAppEvent(string endEvent, Instance instance); +} diff --git a/src/Altinn.App.Core/Internal/App/IAppResources.cs b/src/Altinn.App.Core/Internal/App/IAppResources.cs new file mode 100644 index 000000000..0f1d5d9e7 --- /dev/null +++ b/src/Altinn.App.Core/Internal/App/IAppResources.cs @@ -0,0 +1,174 @@ +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Layout; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.App +{ + /// + /// Interface for execution functionality + /// + public interface IAppResources + { + /// + /// Get the app resource for the given parameters. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// the resource. + /// The app resource. + byte[] GetAppResource(string org, string app, string resource); + + /// + /// Get the app resource for the given parameters. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// the resource. + /// The app resource. + byte[] GetText(string org, string app, string textResource); + + /// + /// Get the text resources in a specific language. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The two letter language code. + /// The text resources in the specified language if they exist. Otherwise null. + Task GetTexts(string org, string app, string language); + + /// + /// Returns the model metadata for an app. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The ServiceMetadata for an app. + [Obsolete("GetModelMetaDataJSON is no longer used by app frontend. Use GetModelJsonSchema.")] + string GetModelMetaDataJSON(string org, string app); + + /// + /// Returns the json schema for the provided model id. + /// + /// Unique identifier for the model. + /// The JSON schema for the model + string GetModelJsonSchema(string modelId); + + /// + /// Method that fetches the runtime resources stored in wwwroot + /// + /// the resource + /// The filestream for the resource file + byte[]? GetRuntimeResource(string resource); + + /// + /// Returns the application metadata for an application. + /// + /// The application metadata for an application. + [Obsolete("GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationMetadata instead", false)] + Application GetApplication(); + + /// + /// Returns the application XACML policy for an application. + /// + /// The application XACML policy for an application. + [Obsolete("GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationXACMLPolicy instead", false)] + string? GetApplicationXACMLPolicy(); + + /// + /// Returns the application BPMN process for an application. + /// + /// The application BPMN process for an application. + [Obsolete("GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMetadata.GetApplicationBPMNProcess instead", false)] + string? GetApplicationBPMNProcess(); + + /// + /// Gets the prefill json file + /// + /// the data model name + /// The prefill json file as a string + string? GetPrefillJson(string dataModelName = "ServiceModel"); + + /// + /// Get the class ref based on data type + /// + /// The datatype + /// Returns the class ref for a given datatype. An empty string is returned if no match is found. + string GetClassRefForLogicDataType(string dataType); + + /// + /// Gets the layouts for the app. + /// + /// A dictionary of FormLayout objects serialized to JSON + string GetLayouts(); + + /// + /// Gets the the layouts settings + /// + /// The layout settings as a JSON string + string? GetLayoutSettingsString(); + + /// + /// Gets the layout settings + /// + /// The layout settings + LayoutSettings GetLayoutSettings(); + + /// + /// Gets the the layout sets + /// + /// The layout sets + string GetLayoutSets(); + + /// + /// Gets the footer layout + /// + /// The footer layout + Task GetFooter(); + + /// + /// Get the layout set definition. Return null if no layoutsets exists + /// + LayoutSets? GetLayoutSet(); + + /// + /// + /// + LayoutSet? GetLayoutSetForTask(string taskId); + + /// + /// Gets the layouts for av given layoutset + /// + /// The layot set id + /// A dictionary of FormLayout objects serialized to JSON + string GetLayoutsForSet(string layoutSetId); + + /// + /// Gets the full layout model for the optional set + /// + LayoutModel GetLayoutModel(string? layoutSetId = null); + + /// + /// Gets the the layouts settings for a layoutset + /// + /// The layot set id + /// The layout settings as a JSON string + string? GetLayoutSettingsStringForSet(string layoutSetId); + + /// + /// Gets the the layouts settings for a layoutset + /// + /// The layout settings + LayoutSettings? GetLayoutSettingsForSet(string? layoutSetId); + + /// + /// Gets the ruleconfiguration for av given layoutset + /// + /// A dictionary of FormLayout objects serialized to JSON + byte[] GetRuleConfigurationForSet(string id); + + /// + /// Gets the the rule handler for a layoutset + /// + /// The layout settings + byte[] GetRuleHandlerForSet(string id); + } +} diff --git a/src/Altinn.App.Core/Internal/App/IApplicationClient.cs b/src/Altinn.App.Core/Internal/App/IApplicationClient.cs new file mode 100644 index 000000000..dd8859081 --- /dev/null +++ b/src/Altinn.App.Core/Internal/App/IApplicationClient.cs @@ -0,0 +1,17 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.App +{ + /// + /// Interface for retrieving application metadata data related operations + /// + public interface IApplicationClient + { + /// + /// Gets the application metdata + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + Task GetApplication(string org, string app); + } +} diff --git a/src/Altinn.App.Core/Internal/Auth/IAuthenticationClient.cs b/src/Altinn.App.Core/Internal/Auth/IAuthenticationClient.cs new file mode 100644 index 000000000..061d7fa33 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Auth/IAuthenticationClient.cs @@ -0,0 +1,14 @@ +namespace Altinn.App.Core.Internal.Auth +{ + /// + /// Authentication interface. + /// + public interface IAuthenticationClient + { + /// + /// Refreshes the AltinnStudioRuntime JwtToken. + /// + /// Response message from Altinn Platform with refreshed token. + Task RefreshToken(); + } +} diff --git a/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs b/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs new file mode 100644 index 000000000..a4d298836 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs @@ -0,0 +1,38 @@ +using System.Security.Claims; +using Altinn.App.Core.Models; +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Core.Internal.Auth +{ + /// + /// Interface for authorization functionality. + /// + public interface IAuthorizationClient + { + /// + /// Returns the list of parties that user has any rights for. + /// + /// The userId. + /// List of parties. + Task?> GetPartyList(int userId); + + /// + /// Verifies that the selected party is contained in the user's party list. + /// + /// The user id. + /// The party id. + /// Boolean indicating whether or not the user can represent the selected party. + Task ValidateSelectedParty(int userId, int partyId); + + /// + /// Check if the user is authorized to perform the given action on the given instance. + /// + /// + /// + /// + /// + /// + /// + Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null); + } +} diff --git a/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs b/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs new file mode 100644 index 000000000..31d873fd0 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Auth/IUserTokenProvider.cs @@ -0,0 +1,18 @@ +#nullable enable + +namespace Altinn.App.Core.Internal.Auth +{ + /// + /// Defines the methods required for an implementation of a user JSON Web Token provider. + /// The provider is used by client implementations that needs the user token in requests + /// against other systems. + /// + public interface IUserTokenProvider + { + /// + /// Defines a method that can return a JSON Web Token of the current user. + /// + /// The Json Web Token for the current user. + public string GetUserToken(); + } +} diff --git a/src/Altinn.App.Core/Internal/Data/IDataClient.cs b/src/Altinn.App.Core/Internal/Data/IDataClient.cs new file mode 100644 index 000000000..aa5a3e9cc --- /dev/null +++ b/src/Altinn.App.Core/Internal/Data/IDataClient.cs @@ -0,0 +1,154 @@ +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.Http; + +namespace Altinn.App.Core.Internal.Data +{ + /// + /// Interface for data handling + /// + public interface IDataClient + { + /// + /// Stores the form model + /// + /// The type + /// The app model to serialize + /// The instance id + /// The type for serialization + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The data type to create, must be a valid data type defined in application metadata + Task InsertFormData(T dataToSerialize, Guid instanceGuid, Type type, string org, string app, int instanceOwnerPartyId, string dataType); + + /// + /// Stores the form + /// + /// The model type + /// The instance that the data element belongs to + /// The data type with requirements + /// The data element instance + /// The class type describing the data + /// The data element metadata + Task InsertFormData(Instance instance, string dataType, T dataToSerialize, Type type); + + /// + /// updates the form data + /// + /// The type + /// The form data to serialize + /// The instanceid + /// The type for serialization + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// the data id + Task UpdateData(T dataToSerialize, Guid instanceGuid, Type type, string org, string app, int instanceOwnerPartyId, Guid dataId); + + /// + /// Gets the form data + /// + /// The instanceid + /// The type for serialization + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// the data id + Task GetFormData(Guid instanceGuid, Type type, string org, string app, int instanceOwnerPartyId, Guid dataId); + + /// + /// Gets the data as is. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instanceid + /// the data id + Task GetBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataId); + + /// + /// Method that gets metadata on form attachments ordered by attachmentType + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// A list with attachments metadata ordered by attachmentType + Task> GetBinaryDataList(string org, string app, int instanceOwnerPartyId, Guid instanceGuid); + + /// + /// Method that removes a form attachments from disk/storage + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The attachment id + [Obsolete("Use method DeleteData with delayed=false instead.", error: true)] + Task DeleteBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid); + + /// + /// Method that removes a data elemen from disk/storage immediatly or marks it as deleted. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The attachment id + /// A boolean indicating whether or not the delete should be executed immediately or delayed + Task DeleteData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid, bool delay); + + /// + /// Method that saves a form attachments to disk/storage and returns the new data element. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The data type to create, must be a valid data type defined in application metadata + /// Http request containing the attachment to be saved + Task InsertBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, string dataType, HttpRequest request); + + /// + /// Method that updates a form attachments to disk/storage and returns the updated data element. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// The instance owner id + /// The instance id + /// The data id + /// Http request containing the attachment to be saved + [Obsolete(message:"Deprecated please use UpdateBinaryData(InstanceIdentifier, string, string, Guid, Stream) instead", error: false)] + Task UpdateBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid, HttpRequest request); + + /// + /// Method that updates a form attachments to disk/storage and returns the updated data element. + /// + /// Instance identifier instanceOwnerPartyId and instanceGuid + /// Content type of the updated binary data + /// Filename of the updated binary data + /// Guid of the data element to update + /// Updated binary data + Task UpdateBinaryData(InstanceIdentifier instanceIdentifier, string? contentType, string filename, Guid dataGuid, Stream stream); + + /// + /// Insert a binary data element. + /// + /// isntanceId = {instanceOwnerPartyId}/{instanceGuid} + /// data type + /// content type + /// filename + /// the stream to stream + /// + Task InsertBinaryData(string instanceId, string dataType, string contentType, string filename, Stream stream); + + /// + /// Updates the data element metadata object. + /// + /// The instance which is not updated + /// The data element with values to update + /// the updated data element + Task Update(Instance instance, DataElement dataElement); + } +} diff --git a/src/Altinn.App.Core/Internal/Events/IEventsClient.cs b/src/Altinn.App.Core/Internal/Events/IEventsClient.cs new file mode 100644 index 000000000..5888ec778 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Events/IEventsClient.cs @@ -0,0 +1,15 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Events +{ + /// + /// Interface describing client implementations for the Events component in the Altinn 3 platform. + /// + public interface IEventsClient + { + /// + /// Adds a new event to the events published by the Events component. + /// + Task AddEvent(string eventType, Instance instance); + } +} diff --git a/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs b/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs index a8bf5234d..c204b574c 100644 --- a/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs +++ b/src/Altinn.App.Core/Internal/Events/KeyVaultSecretCodeProvider.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Secrets; namespace Altinn.App.Core.Internal.Events { @@ -8,7 +8,7 @@ namespace Altinn.App.Core.Internal.Events /// public class KeyVaultEventSecretCodeProvider : IEventSecretCodeProvider { - private readonly ISecrets _keyVaultClient; + private readonly ISecretsClient _keyVaultClient; private string _secretCode = string.Empty; /// @@ -16,7 +16,7 @@ public class KeyVaultEventSecretCodeProvider : IEventSecretCodeProvider /// This /// /// - public KeyVaultEventSecretCodeProvider(ISecrets keyVaultClient) + public KeyVaultEventSecretCodeProvider(ISecretsClient keyVaultClient) { _keyVaultClient = keyVaultClient; } diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs index db51ff9d8..72ed7bd35 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs @@ -1,8 +1,6 @@ -using System.Text.Json; -using Altinn.App.Core.Interface; using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers.DataModel; -using Altinn.App.Core.Models.Layout; +using Altinn.App.Core.Internal.App; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Options; diff --git a/src/Altinn.App.Core/Internal/Instances/IInstanceClient.cs b/src/Altinn.App.Core/Internal/Instances/IInstanceClient.cs new file mode 100644 index 000000000..e64640bb7 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Instances/IInstanceClient.cs @@ -0,0 +1,135 @@ +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Primitives; + +namespace Altinn.App.Core.Internal.Instances +{ + /// + /// Interface for handling form data related operations + /// + public interface IInstanceClient + { + /// + /// Gets the instance + /// + Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceId); + + /// + /// Gets the instance anew. Instance must have set appId, instanceOwner.PartyId and Id. + /// + Task GetInstance(Instance instance); + + /// + /// Gets a list of instances based on a dictionary of provided query parameters. + /// + Task> GetInstances(Dictionary queryParams); + + /// + /// Updates the process model of the instance and returns the updated instance. + /// + Task UpdateProcess(Instance instance); + + /// + /// Creates an instance of an application with no data. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// the instance template to create (must have instanceOwner with partyId, personNumber or organisationNumber set) + /// The created instance + Task CreateInstance(string org, string app, Instance instanceTemplate); + + /// + /// Add complete confirmation. + /// + /// + /// Add to an instance that a given stakeholder considers the instance as no longer needed by them. The stakeholder has + /// collected all the data and information they needed from the instance and expect no additional data to be added to it. + /// The body of the request isn't used for anything despite this being a POST operation. + /// + /// The party id of the instance owner. + /// The id of the instance to confirm as complete. + /// Returns the updated instance. + Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid); + + /// + /// Update read status. + /// + /// The party id of the instance owner. + /// The id of the instance to confirm as complete. + /// The new instance read status. + /// Returns the updated instance. + Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus); + + /// + /// Update substatus. + /// + /// The party id of the instance owner. + /// The id of the instance to be updated. + /// The new substatus. + /// Returns the updated instance. + Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus); + + /// + /// Update presentation texts. + /// + /// + /// The provided presentation texts will be merged with the existing collection of presentation texts on the instance. + /// + /// The party id of the instance owner. + /// The id of the instance to update presentation texts for. + /// The presentation texts + /// Returns the updated instance. + Task UpdatePresentationTexts(int instanceOwnerPartyId, Guid instanceGuid, PresentationTexts presentationTexts); + + /// + /// Update data values. + /// + /// + /// The provided data values will be merged with the existing collection of data values on the instance. + /// + /// The party id of the instance owner. + /// The id of the instance to update data values for. + /// The data values + /// Returns the updated instance. + Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues); + + /// + /// Update data data values. + /// + /// + /// The provided data value will be added with the existing collection of data values on the instance. + /// + /// The instance + /// The data value (null unsets the value) + /// Returns the updated instance. + async Task UpdateDataValues(Instance instance, Dictionary dataValues) + { + var id = new InstanceIdentifier(instance); + return await UpdateDataValues(id.InstanceOwnerPartyId, id.InstanceGuid, new DataValues{Values = dataValues}); + } + + /// + /// Update single data value. + /// + /// + /// The provided data value will be added with the existing collection of data values on the instance. + /// + /// The instance + /// The key of the DataValues collection to be updated. + /// The data value (null unsets the value) + /// Returns the updated instance. + async Task UpdateDataValue(Instance instance, string key, string? value) + { + return await UpdateDataValues(instance, new Dictionary{{key, value}}); + } + + /// + /// Delete instance. + /// + /// The party id of the instance owner. + /// The id of the instance to delete. + /// Boolean to indicate if instance should be hard deleted. + /// Returns the deleted instance. + Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard); + } +} diff --git a/src/Altinn.App.Core/Internal/Instances/IInstanceEventClient.cs b/src/Altinn.App.Core/Internal/Instances/IInstanceEventClient.cs new file mode 100644 index 000000000..1b4f4e81a --- /dev/null +++ b/src/Altinn.App.Core/Internal/Instances/IInstanceEventClient.cs @@ -0,0 +1,20 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Instances +{ + /// + /// Interface for handling instance event related operations + /// + public interface IInstanceEventClient + { + /// + /// Stores the instance event + /// + Task SaveInstanceEvent(object dataToSerialize, string org, string app); + + /// + /// Gets the instance events related to the instance matching the instance id. + /// + Task> GetInstanceEvents(string instanceId, string instanceOwnerPartyId, string org, string app, string[] eventTypes, string from, string to); + } +} diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index de1251bd4..48813a19f 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -1,11 +1,13 @@ using System.Security.Claims; -using System.Text; using System.Xml.Serialization; using Altinn.App.Core.Configuration; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; using Altinn.App.Core.Helpers.Extensions; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; @@ -27,10 +29,10 @@ public class PdfService : IPdfService private readonly IPDF _pdfClient; private readonly IAppResources _resourceService; private readonly IPdfOptionsMapping _pdfOptionsMapping; - private readonly IData _dataClient; + private readonly IDataClient _dataClient; private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IProfile _profileClient; - private readonly IRegister _registerClient; + private readonly IProfileClient _profileClient; + private readonly IAltinnPartyClient _altinnPartyClientClient; private readonly IPdfFormatter _pdfFormatter; private readonly IPdfGeneratorClient _pdfGeneratorClient; @@ -49,7 +51,7 @@ public class PdfService : IPdfService /// The data client. /// The httpContextAccessor /// The profile client - /// The register client + /// The register client /// Class for customizing pdf formatting and layout. /// PDF generator client for the experimental PDF generator service /// PDF generator related settings. @@ -58,10 +60,10 @@ public PdfService( IPDF pdfClient, IAppResources appResources, IPdfOptionsMapping pdfOptionsMapping, - IData dataClient, + IDataClient dataClient, IHttpContextAccessor httpContextAccessor, - IProfile profileClient, - IRegister registerClient, + IProfileClient profileClient, + IAltinnPartyClient altinnPartyClientClient, IPdfFormatter pdfFormatter, IPdfGeneratorClient pdfGeneratorClient, IOptions pdfGeneratorSettings, @@ -74,7 +76,7 @@ IOptions generalSettings _dataClient = dataClient; _httpContextAccessor = httpContextAccessor; _profileClient = profileClient; - _registerClient = registerClient; + _altinnPartyClientClient = altinnPartyClientClient; _pdfFormatter = pdfFormatter; _pdfGeneratorClient = pdfGeneratorClient; _pdfGeneratorSettings = pdfGeneratorSettings.Value; @@ -191,7 +193,7 @@ public async Task GenerateAndStoreReceiptPDF(Instance instance, string taskId, D else { string? orgNumber = user.GetOrgNumber().ToString(); - actingParty = await _registerClient.LookupParty(new PartyLookup { OrgNo = orgNumber }); + actingParty = await _altinnPartyClientClient.LookupParty(new PartyLookup { OrgNo = orgNumber }); } // If layoutset exists pick correct layotFiles @@ -216,7 +218,7 @@ public async Task GenerateAndStoreReceiptPDF(Instance instance, string taskId, D LayoutSettings = layoutSettings, TextResources = JsonConvert.DeserializeObject(textResourcesString)!, OptionsDictionary = optionsDictionary, - Party = await _registerClient.GetParty(instanceOwnerId), + Party = await _altinnPartyClientClient.GetParty(instanceOwnerId), Instance = instance, UserParty = actingParty, Language = language diff --git a/src/Altinn.App.Core/Internal/Prefill/IPrefill.cs b/src/Altinn.App.Core/Internal/Prefill/IPrefill.cs new file mode 100644 index 000000000..3fcb13ae2 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Prefill/IPrefill.cs @@ -0,0 +1,25 @@ +namespace Altinn.App.Core.Internal.Prefill +{ + /// + /// The prefill service + /// + public interface IPrefill + { + /// + /// Prefills the data model based on key/values in the dictionary. + /// + /// The data model object + /// External given prefill + /// Ignore errors when true, throw on errors when false + void PrefillDataModel(object dataModel, Dictionary externalPrefill, bool continueOnError = false); + + /// + /// Prefills the data model based on the prefill json configuration file + /// + /// The partyId of the instance owner + /// The data model name + /// The data model object + /// External given prefill + Task PrefillDataModel(string partyId, string dataModelName, object dataModel, Dictionary? externalPrefill = null); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs index 15f37be3e..7450dd04b 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs @@ -10,7 +10,7 @@ public class AltinnAction /// /// Gets or sets the ID of the action /// - [XmlAttribute("id")] + [XmlAttribute("id", Namespace = "http://altinn.no/process")] public string Id { get; set; } } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnGatewayExtension.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnGatewayExtension.cs new file mode 100644 index 000000000..556a60f4d --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnGatewayExtension.cs @@ -0,0 +1,16 @@ +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties +{ + /// + /// Defines the altinn properties for a task + /// + public class AltinnGatewayExtension + { + /// + /// Gets or sets the data type id connected to the task + /// + [XmlElement("connectedDataTypeId", Namespace = "http://altinn.no/process", IsNullable = true)] + public string? ConnectedDataTypeId { get; set; } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnProperties.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnProperties.cs deleted file mode 100644 index f2f90bde1..000000000 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnProperties.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Xml.Serialization; - -namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties -{ - /// - /// Defines the altinn properties for a task - /// - public class AltinnProperties - { - /// - /// List of available actions for a task - /// - [XmlArray(ElementName = "actions", Namespace = "http://altinn.no", IsNullable = true)] - [XmlArrayItem(ElementName = "action", Namespace = "http://altinn.no")] - public List? AltinnActions { get; set; } - - /// - /// Gets or sets the task type - /// - //[XmlElement(ElementName = "taskType", Namespace = "http://altinn.no", IsNullable = true)] - [XmlElement("taskType", Namespace = "http://altinn.no")] - public string? TaskType { get; set; } - - /// - /// Gets or sets the data type id connected to the task - /// - [XmlElement("connectedDataTypeId", Namespace = "http://altinn.no", IsNullable = true)] - public string? ConnectedDataTypeId { get; set; } - } -} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs new file mode 100644 index 000000000..d45bf9f73 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs @@ -0,0 +1,37 @@ +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties +{ + /// + /// Defines the altinn properties for a task + /// + public class AltinnTaskExtension + { + /// + /// List of available actions for a task + /// + [XmlArray(ElementName = "actions", Namespace = "http://altinn.no/process", IsNullable = true)] + [XmlArrayItem(ElementName = "action", Namespace = "http://altinn.no/process")] + public List? AltinnActions { get; set; } + + /// + /// Gets or sets the task type + /// + //[XmlElement(ElementName = "taskType", Namespace = "http://altinn.no/process/task", IsNullable = true)] + [XmlElement("taskType", Namespace = "http://altinn.no/process")] + public string? TaskType { get; set; } + + /// + /// Define what taskId that should be signed for signing tasks + /// + [XmlArray(ElementName = "dataTypesToSign", Namespace = "http://altinn.no/process", IsNullable = true)] + [XmlArrayItem(ElementName = "dataType", Namespace = "http://altinn.no/process")] + public List DataTypesToSign { get; set; } = new(); + + /// + /// Set what dataTypeId that should be used for storing the signature + /// + [XmlElement("signatureDataType", Namespace = "http://altinn.no/process")] + public string SignatureDataType { get; set; } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs index 743d44f6d..8e823501f 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ConfirmationTask.cs @@ -1,5 +1,3 @@ -using Altinn.App.Core.Interface; -using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements diff --git a/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs index 87c93a8e9..4821f61c5 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/DataTask.cs @@ -1,5 +1,3 @@ -using Altinn.App.Core.Interface; -using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs b/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs index 1f624b611..d5db11e70 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ExtensionElements.cs @@ -11,7 +11,13 @@ public class ExtensionElements /// /// Gets or sets the altinn properties /// - [XmlElement("properties", Namespace = "http://altinn.no")] - public AltinnProperties? AltinnProperties { get; set; } + [XmlElement("taskExtension", Namespace = "http://altinn.no/process")] + public AltinnTaskExtension? TaskExtension { get; set; } + + /// + /// Gets or sets the altinn properties + /// + [XmlElement("gatewayExtension", Namespace = "http://altinn.no/process")] + public AltinnGatewayExtension? GatewayExtension { get; set; } } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs index 49ba7ee07..c5ccdf41a 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/FeedbackTask.cs @@ -1,5 +1,3 @@ -using Altinn.App.Core.Interface; -using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Process.Elements diff --git a/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs b/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs index de39c1fa8..26c441a45 100644 --- a/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs +++ b/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs @@ -1,9 +1,9 @@ using System.Text; using System.Text.Json; using Altinn.App.Core.Features; -using Altinn.App.Core.Interface; 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.Elements; using Altinn.App.Core.Models; @@ -21,7 +21,7 @@ public class ExpressionsExclusiveGateway : IProcessExclusiveGateway private readonly LayoutEvaluatorStateInitializer _layoutStateInit; private readonly IAppResources _resources; private readonly IAppMetadata _appMetadata; - private readonly IData _dataClient; + private readonly IDataClient _dataClient; private readonly IAppModel _appModel; /// @@ -37,7 +37,7 @@ public ExpressionsExclusiveGateway( IAppResources resources, IAppModel appModel, IAppMetadata appMetadata, - IData dataClient) + IDataClient dataClient) { _layoutStateInit = layoutEvaluatorStateInitializer; _resources = resources; diff --git a/src/Altinn.App.Core/Internal/Process/IProcessClient.cs b/src/Altinn.App.Core/Internal/Process/IProcessClient.cs new file mode 100644 index 000000000..d204ffcf5 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/IProcessClient.cs @@ -0,0 +1,21 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Internal.Process +{ + /// + /// Process service that encapsulate reading of the BPMN process definition. + /// + public interface IProcessClient + { + /// + /// Returns a stream that contains the process definition. + /// + /// the stream + Stream GetProcessDefinition(); + + /// + /// Gets the instance process events related to the instance matching the instance id. + /// + Task GetProcessHistory(string instanceGuid, string instanceOwnerPartyId); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ITaskEvents.cs b/src/Altinn.App.Core/Internal/Process/ITaskEvents.cs new file mode 100644 index 000000000..87c8cbb6d --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ITaskEvents.cs @@ -0,0 +1,34 @@ +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/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 7e550c62d..b206f5d54 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -1,10 +1,12 @@ using System.Security.Claims; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features.Action; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Models.Process; +using Altinn.App.Core.Models.UserAction; using Altinn.Platform.Profile.Models; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; @@ -17,27 +19,31 @@ namespace Altinn.App.Core.Internal.Process; public class ProcessEngine : IProcessEngine { private readonly IProcessReader _processReader; - private readonly IProfile _profileService; + private readonly IProfileClient _profileClient; private readonly IProcessNavigator _processNavigator; private readonly IProcessEventDispatcher _processEventDispatcher; + private readonly UserActionFactory _userActionFactory; /// /// Initializes a new instance of the class /// - /// - /// - /// - /// + /// Process reader service + /// The profile service + /// The process navigator + /// The process event dispatcher + /// The action handler factory public ProcessEngine( IProcessReader processReader, - IProfile profileService, + IProfileClient profileClient, IProcessNavigator processNavigator, - IProcessEventDispatcher processEventDispatcher) + IProcessEventDispatcher processEventDispatcher, + UserActionFactory userActionFactory) { _processReader = processReader; - _profileService = profileService; + _profileClient = profileClient; _processNavigator = processNavigator; _processEventDispatcher = processEventDispatcher; + _userActionFactory = userActionFactory; } /// @@ -74,10 +80,12 @@ public async Task StartProcess(ProcessStartRequest processS { events.Add(startEvent); } + if (goToNextEvent is not null) { events.Add(goToNextEvent); } + ProcessStateChange processStateChange = new ProcessStateChange { OldProcessState = startChange?.OldProcessState, @@ -96,7 +104,7 @@ public async Task StartProcess(ProcessStartRequest processS ProcessStateChange = processStateChange }; } - + /// public async Task Next(ProcessNextRequest request) { @@ -113,6 +121,28 @@ public async Task Next(ProcessNextRequest request) }; } + int? userId = request.User.GetUserIdAsInt(); + if (userId == null) + { + return new ProcessChangeResult() + { + Success = false, + ErrorMessage = $"User does not have a valid user id!", + ErrorType = ProcessErrorType.Conflict + }; + } + var actionHandler = await _userActionFactory.GetActionHandler(request.Action).HandleAction(new UserActionContext(request.Instance, userId.Value)); + + if (!actionHandler) + { + return new ProcessChangeResult() + { + Success = false, + ErrorMessage = $"Action handler for action {request.Action} failed!", + ErrorType = ProcessErrorType.Internal + }; + } + var nextResult = await HandleMoveToNext(instance, request.User, request.Action); return new ProcessChangeResult() @@ -141,7 +171,7 @@ public async Task UpdateInstanceAndRerunEvents(ProcessStartRequest sta { Started = now, StartEvent = startEvent, - CurrentTask = new ProcessElementInfo { Flow = 1, ElementId = startEvent} + CurrentTask = new ProcessElementInfo { Flow = 1, ElementId = startEvent } }; instance.Process = startState; @@ -186,7 +216,7 @@ await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString() return null; } - + private async Task> MoveProcessToNext( Instance instance, ClaimsPrincipal user, @@ -209,6 +239,7 @@ private async Task> MoveProcessToNext( { eventType = InstanceEventType.process_AbandonTask.ToString(); } + events.Add(await GenerateProcessChangeEvent(eventType, instance, now, user)); instance.Process = currentState; } @@ -234,7 +265,7 @@ private async Task> MoveProcessToNext( ElementId = nextElement!.Id, Name = nextElement!.Name, Started = now, - AltinnTaskType = task?.ExtensionElements?.AltinnProperties?.TaskType, + AltinnTaskType = task?.ExtensionElements?.TaskExtension?.TaskType, Validated = null, }; @@ -267,13 +298,13 @@ private async Task GenerateProcessChangeEvent(string eventType, I if (string.IsNullOrEmpty(instanceEvent.User.OrgId) && userId != null) { - UserProfile up = await _profileService.GetUserProfile((int)userId); + UserProfile up = await _profileClient.GetUserProfile((int)userId); instanceEvent.User.NationalIdentityNumber = up.Party.SSN; } return instanceEvent; } - + private async Task HandleMoveToNext(Instance instance, ClaimsPrincipal user, string? action) { var processStateChange = await ProcessNext(instance, user, action); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs b/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs index 81d4c0b28..25dabace8 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs @@ -1,5 +1,7 @@ using Altinn.App.Core.Configuration; -using Altinn.App.Core.Interface; +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; @@ -13,28 +15,28 @@ namespace Altinn.App.Core.Internal.Process; /// class ProcessEventDispatcher : IProcessEventDispatcher { - private readonly IInstance _instanceService; - private readonly IInstanceEvent _instanceEventClient; + private readonly IInstanceClient _instanceClient; + private readonly IInstanceEventClient _instanceEventClient; private readonly ITaskEvents _taskEvents; private readonly IAppEvents _appEvents; - private readonly IEvents _eventsService; + private readonly IEventsClient _eventsClient; private readonly bool _registerWithEventSystem; private readonly ILogger _logger; public ProcessEventDispatcher( - IInstance instanceService, - IInstanceEvent instanceEventClient, + IInstanceClient instanceClient, + IInstanceEventClient instanceEventClient, ITaskEvents taskEvents, IAppEvents appEvents, - IEvents eventsService, + IEventsClient eventsClient, IOptions appSettings, ILogger logger) { - _instanceService = instanceService; + _instanceClient = instanceClient; _instanceEventClient = instanceEventClient; _taskEvents = taskEvents; _appEvents = appEvents; - _eventsService = eventsService; + _eventsClient = eventsClient; _registerWithEventSystem = appSettings.Value.RegisterEventsWithEventsComponent; _logger = logger; } @@ -45,11 +47,11 @@ public async Task UpdateProcessAndDispatchEvents(Instance instance, Di 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 _instanceService.UpdateProcess(instance); + Instance updatedInstance = await _instanceClient.UpdateProcess(instance); await DispatchProcessEventsToStorage(updatedInstance, events); // remember to get the instance anew since AppBase can have updated a data element or stored something in the database. - updatedInstance = await _instanceService.GetInstance(updatedInstance); + updatedInstance = await _instanceClient.GetInstance(updatedInstance); return updatedInstance; } @@ -63,11 +65,11 @@ public async Task RegisterEventWithEventsComponent(Instance instance) { if (!string.IsNullOrWhiteSpace(instance.Process.CurrentTask?.ElementId)) { - await _eventsService.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) { - await _eventsService.AddEvent("app.instance.process.completed", instance); + await _eventsClient.AddEvent("app.instance.process.completed", instance); } } catch (Exception exception) @@ -120,7 +122,7 @@ private async Task HandleProcessChanges(Instance instance, List? break; case InstanceEventType.process_AbandonTask: await task.HandleTaskAbandon(elementId, instance); - await _instanceService.UpdateProcess(instance); + await _instanceClient.UpdateProcess(instance); break; case InstanceEventType.process_EndEvent: await _appEvents.OnEndAppEvent(instanceEvent.ProcessInfo?.EndEvent, instance); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs b/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs index 5b2b9af33..1bb44a063 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessNavigator.cs @@ -84,7 +84,7 @@ private async Task> NextFollowAndFilterGateways(Instance in ProcessGatewayInformation gatewayInformation = new() { Action = action, - DataTypeId = gateway.ExtensionElements?.AltinnProperties?.ConnectedDataTypeId + DataTypeId = gateway.ExtensionElements?.GatewayExtension?.ConnectedDataTypeId }; filteredList = await gatewayFilter.FilterAsync(outgoingFlows, instance, gatewayInformation); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs index ac2573cbd..88c17beeb 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs @@ -1,5 +1,4 @@ using System.Xml.Serialization; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.Base; @@ -13,14 +12,14 @@ public class ProcessReader : IProcessReader private readonly Definitions _definitions; /// - /// Create instance of ProcessReader where process stream is fetched from + /// Create instance of ProcessReader where process stream is fetched from /// - /// Implementation of IProcess used to get stream of BPMN process + /// Implementation of IProcessClient used to get stream of BPMN process /// If BPMN file could not be deserialized - public ProcessReader(IProcess processService) + public ProcessReader(IProcessClient processClient) { XmlSerializer serializer = new XmlSerializer(typeof(Definitions)); - Definitions? definitions = (Definitions?)serializer.Deserialize(processService.GetProcessDefinition()); + Definitions? definitions = (Definitions?)serializer.Deserialize(processClient.GetProcessDefinition()); _definitions = definitions ?? throw new InvalidOperationException("Failed to deserialize BPMN definitions. Definitions was null"); } diff --git a/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs b/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs new file mode 100644 index 000000000..d6e5cf181 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs @@ -0,0 +1,17 @@ +using Altinn.Platform.Profile.Models; + +namespace Altinn.App.Core.Internal.Profile +{ + /// + /// Interface for profile functionality + /// + public interface IProfileClient + { + /// + /// Method for getting the userprofile from a given user id + /// + /// the user id + /// The userprofile for the given user id + Task GetUserProfile(int userId); + } +} diff --git a/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs b/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs new file mode 100644 index 000000000..d1e4e12c6 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs @@ -0,0 +1,24 @@ +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Core.Internal.Registers +{ + /// + /// Interface for register functionality + /// + public interface IAltinnPartyClient + { + /// + /// Returns party information + /// + /// The partyId + /// The party for the given partyId + Task GetParty(int partyId); + + /// + /// Looks up a party by person or organisation number. + /// + /// A populated lookup object with information about what to look for. + /// The party lookup containing either SSN or organisation number. + Task LookupParty(PartyLookup partyLookup); + } +} diff --git a/src/Altinn.App.Core/Internal/Registers/IOrganizationClient.cs b/src/Altinn.App.Core/Internal/Registers/IOrganizationClient.cs new file mode 100644 index 000000000..5b18de260 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Registers/IOrganizationClient.cs @@ -0,0 +1,17 @@ +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Core.Internal.Registers +{ + /// + /// Interface for the entity registry (ER: Enhetsregisteret) + /// + public interface IOrganizationClient + { + /// + /// Method for getting an organization based on a organization nr + /// + /// the organization number + /// The organization for the given organization number + Task GetOrganization(string OrgNr); + } +} diff --git a/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs b/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs new file mode 100644 index 000000000..726f88e0c --- /dev/null +++ b/src/Altinn.App.Core/Internal/Registers/IPersonClient.cs @@ -0,0 +1,25 @@ +#nullable enable + +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Core.Internal.Registers +{ + /// + /// Describes the required methods for an implementation of a person repository client. + /// + public interface IPersonClient + { + /// + /// Get the object for the person identified with the parameters. + /// + /// + /// The method requires both the national identity number and the last name of the person. This is used to + /// verify that entered information is correct and to prevent testing of random identity numbers. + /// + /// The national identity number of the person. + /// The last name of the person. + /// The cancellation token to cancel operation. + /// The identified person if found. + Task GetPerson(string nationalIdentityNumber, string lastName, CancellationToken ct); + } +} diff --git a/src/Altinn.App.Core/Internal/Secrets/ISecretsClient.cs b/src/Altinn.App.Core/Internal/Secrets/ISecretsClient.cs new file mode 100644 index 000000000..0fcd06e1a --- /dev/null +++ b/src/Altinn.App.Core/Internal/Secrets/ISecretsClient.cs @@ -0,0 +1,38 @@ +using Microsoft.Azure.KeyVault; +using Microsoft.Azure.KeyVault.WebKey; + +namespace Altinn.App.Core.Internal.Secrets +{ + /// + /// Interface for secrets service + /// + public interface ISecretsClient + { + /// + /// Gets the latest version of a key from key vault. + /// + /// The name of the key. + /// The key as a JSON web key. + Task GetKeyAsync(string keyName); + + /// + /// Gets the latest version of a secret from key vault. + /// + /// The name of the secret. + /// The secret value. + Task GetSecretAsync(string secretName); + + /// + /// Gets the latest version of a certificate from key vault. + /// + /// The name of certificate. + /// The certificate as a byte array. + Task GetCertificateAsync(string certificateName); + + /// + /// Gets the key vault client. + /// + /// The key vault client. + KeyVaultClient GetKeyVaultClient(); + } +} diff --git a/src/Altinn.App.Core/Internal/Sign/ISignClient.cs b/src/Altinn.App.Core/Internal/Sign/ISignClient.cs new file mode 100644 index 000000000..c7185cc6b --- /dev/null +++ b/src/Altinn.App.Core/Internal/Sign/ISignClient.cs @@ -0,0 +1,16 @@ +using Altinn.App.Core.Models; + +namespace Altinn.App.Core.Internal.Sign; + +/// +/// Interface for httpClient to send sign requests to platform +/// +public interface ISignClient +{ + /// + /// Generate a signature for a list of DataElements for a user + /// + /// The context for the signature + /// + public Task SignDataElements(SignatureContext signatureContext); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs b/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs new file mode 100644 index 000000000..d4635c138 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs @@ -0,0 +1,117 @@ +using Altinn.App.Core.Models; + +namespace Altinn.App.Core.Internal.Sign; + +/// +/// Context for a signature of DataElements +/// +public class SignatureContext +{ + /// + /// Create a new signing context for one data element + /// + /// Identifier for the instance containing the data elements to sign + /// The id of the DataType where the signature should be stored + /// The signee + /// The data element to sign + public SignatureContext(InstanceIdentifier instanceIdentifier, string signatureDataTypeId, Signee signee, params DataElementSignature[] dataElementSignature) + { + InstanceIdentifier = instanceIdentifier; + SignatureDataTypeId = signatureDataTypeId; + DataElementSignatures.AddRange(dataElementSignature); + Signee = signee; + } + + /// + /// Create a new signing context for multiple data elements + /// + /// Identifier for the instance containing the data elements to sign + /// The id of the DataType where the signature should be stored + /// The signee + /// The data elements to sign + public SignatureContext(InstanceIdentifier instanceIdentifier, string signatureDataTypeId, Signee signee, List dataElementSignatures) + { + InstanceIdentifier = instanceIdentifier; + SignatureDataTypeId = signatureDataTypeId; + DataElementSignatures = dataElementSignatures; + Signee = signee; + } + + /// + /// The id of the DataType where the signature should be stored + /// + public string SignatureDataTypeId { get; } + + /// + /// Identifier for the instance containing the data elements to sign + /// + public InstanceIdentifier InstanceIdentifier { get; } + + /// + /// List of DataElements and whether they are signed or not + /// + public List DataElementSignatures { get; } = new (); + + /// + /// The user performing the signing + /// + public Signee Signee { get; } +} + +/// +/// Object representing the user performing the signing +/// +public class Signee +{ + /// + /// User id of the user performing the signing + /// + public string UserId { get; set; } + + /// + /// The SSN of the user performing the signing, set if the signer is a person + /// + public string? PersonNumber { get; set; } + + /// + /// The organisation number of the user performing the signing, set if the signer is an organisation + /// + public string? OrganisationNumber { get; set; } +} + +/// +/// Object representing a data element and whether it is signed or not +/// +public class DataElementSignature +{ + /// + /// Create a new data element where the signed status is set to true + /// + /// ID of the DataElement that should be included in the signature + public DataElementSignature(string dataElementId) + { + DataElementId = dataElementId; + Signed = true; + } + + /// + /// Create a new data element where the signed status is set to the value of the signed parameter + /// + /// ID of the DataElement that should be included in the signature + /// Whether the DataElement is signed or not + public DataElementSignature(string dataElementId, bool signed) + { + DataElementId = dataElementId; + Signed = signed; + } + + /// + /// ID of the DataElement that should be included in the signature + /// + public string DataElementId { get; } + + /// + /// Whether the DataElement is signed or not + /// + public bool Signed { get; } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs b/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs index d7981f48b..4ae14a0a6 100644 --- a/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs +++ b/src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs @@ -32,6 +32,16 @@ public enum ProcessErrorType /// /// The process change was not allowed due to the current state of the process /// - Conflict + Conflict, + + /// + /// The process change lead to an internal error + /// + Internal, + + /// + /// The user is not authorized to perform the process change + /// + Unauthorized } } diff --git a/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs b/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs new file mode 100644 index 000000000..1f8ea3136 --- /dev/null +++ b/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs @@ -0,0 +1,16 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Models.UserAction; + +public class UserActionContext +{ + public UserActionContext(Instance instance, int userId) + { + Instance = instance; + UserId = userId; + } + + public Instance Instance { get; } + + public int UserId { get; } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs index 72ff8f5b6..0092901cd 100644 --- a/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs @@ -1,12 +1,10 @@ using Altinn.App.Api.Controllers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Infrastructure.Clients; +using Altinn.App.Core.Internal.Instances; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.AspNetCore.Mvc; using Moq; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Xunit; namespace Altinn.App.Api.Tests.Controllers @@ -20,7 +18,7 @@ public async Task InstanceAndDataExists_ShouldReturn200Ok() const string app = "app"; const int instanceOwnerPartyId = 12345; Guid instanceId = Guid.NewGuid(); - Mock instanceClientMock = CreateInstanceClientMock(org, app, instanceOwnerPartyId, instanceId); + Mock instanceClientMock = CreateInstanceClientMock(org, app, instanceOwnerPartyId, instanceId); var fileScanController = new FileScanController(instanceClientMock.Object); var fileScanResults = await fileScanController.GetFileScanResults(org, app, instanceOwnerPartyId, instanceId); @@ -36,7 +34,7 @@ public async Task InstanceDoesNotExists_ShouldReturnNotFound() const string app = "app"; const int instanceOwnerPartyId = 12345; Guid instanceId = Guid.NewGuid(); - Mock instanceClientMock = CreateInstanceClientMock(org, app, instanceOwnerPartyId, instanceId); + Mock instanceClientMock = CreateInstanceClientMock(org, app, instanceOwnerPartyId, instanceId); var fileScanController = new FileScanController(instanceClientMock.Object); var fileScanResults = await fileScanController.GetFileScanResults(org, app, instanceOwnerPartyId, Guid.NewGuid()); @@ -44,7 +42,7 @@ public async Task InstanceDoesNotExists_ShouldReturnNotFound() fileScanResults.Result.Should().BeOfType(); } - private static Mock CreateInstanceClientMock(string org, string app, int instanceOwnerPartyId, Guid instanceId) + private static Mock CreateInstanceClientMock(string org, string app, int instanceOwnerPartyId, Guid instanceId) { var instance = new Instance { @@ -56,7 +54,7 @@ private static Mock CreateInstanceClientMock(string org, string app, } }; - var instanceClientMock = new Mock(); + var instanceClientMock = new Mock(); instanceClientMock .Setup(e => e.GetInstance(app, org, instanceOwnerPartyId, instanceId)) .Returns(Task.FromResult(instance)); diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs index e86bea762..909bc8360 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs @@ -2,7 +2,6 @@ using Altinn.App.Api.Controllers; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.AppModel; using Altinn.Common.PEP.Interfaces; using Altinn.Platform.Storage.Interface.Models; @@ -14,6 +13,12 @@ using Xunit; using Altinn.App.Api.Models; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; using IProcessEngine = Altinn.App.Core.Internal.Process.IProcessEngine; @@ -23,19 +28,20 @@ namespace Altinn.App.Api.Tests.Controllers; public class InstancesController_ActiveInstancesTest { private readonly Mock> _logger = new(); - private readonly Mock _registrer = new(); - private readonly Mock _instanceClient = new(); - private readonly Mock _data = new(); + private readonly Mock _registrer = new(); + private readonly Mock _instanceClient = new(); + private readonly Mock _data = new(); private readonly Mock _appMetadata = new(); private readonly Mock _appModel = new(); private readonly Mock _instantiationProcessor = new(); private readonly Mock _instantiationValidator = new(); private readonly Mock _pdp = new(); - private readonly Mock _eventsService = new(); + private readonly Mock _eventsService = new(); private readonly IOptions _appSettings = Options.Create(new()); private readonly Mock _prefill = new(); - private readonly Mock _profile = new(); + private readonly Mock _profile = new(); private readonly Mock _processEngine = new(); + private readonly Mock _oarganizationClientMock = new(); private InstancesController SUT => new InstancesController( _logger.Object, @@ -51,7 +57,8 @@ public class InstancesController_ActiveInstancesTest _appSettings, _prefill.Object, _profile.Object, - _processEngine.Object); + _processEngine.Object, + _oarganizationClientMock.Object); private void VerifyNoOtherCalls() { @@ -286,7 +293,7 @@ public async Task LastChangedBy9digits_LooksForOrg() }); _instanceClient.Setup(c => c.GetInstances(It.IsAny>())).ReturnsAsync(instances); - _registrer.Setup(r => r.ER.GetOrganization("123456789")).ReturnsAsync(default(Organization)); + _oarganizationClientMock.Setup(er => er.GetOrganization("123456789")).ReturnsAsync(default(Organization)); // Act var controller = SUT; @@ -300,7 +307,7 @@ public async Task LastChangedBy9digits_LooksForOrg() _instanceClient.Verify(c => c.GetInstances(It.Is>(query => query.ContainsKey("appId") ))); - _registrer.Verify(r => r.ER.GetOrganization("123456789")); + _oarganizationClientMock.Verify(er => er.GetOrganization("123456789")); VerifyNoOtherCalls(); } @@ -333,7 +340,7 @@ public async Task LastChangedBy9digits_FindsOrg() }); _instanceClient.Setup(c => c.GetInstances(It.IsAny>())).ReturnsAsync(instances); - _registrer.Setup(r => r.ER.GetOrganization("123456789")).ReturnsAsync(new Organization + _oarganizationClientMock.Setup(er => er.GetOrganization("123456789")).ReturnsAsync(new Organization { Name = "Testdepartementet" }); @@ -350,7 +357,7 @@ public async Task LastChangedBy9digits_FindsOrg() _instanceClient.Verify(c => c.GetInstances(It.Is>(query => query.ContainsKey("appId") ))); - _registrer.Verify(r => r.ER.GetOrganization("123456789")); + _oarganizationClientMock.Verify(er => er.GetOrganization("123456789")); VerifyNoOtherCalls(); } } diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs index 84ac2ac03..922333917 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs @@ -3,9 +3,14 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.Validation; @@ -28,20 +33,21 @@ namespace Altinn.App.Api.Tests.Controllers; public class InstancesController_CopyInstanceTests { private readonly Mock> _logger = new(); - private readonly Mock _registrer = new(); - private readonly Mock _instanceClient = new(); - private readonly Mock _data = new(); + private readonly Mock _registrer = new(); + private readonly Mock _instanceClient = new(); + private readonly Mock _data = new(); private readonly Mock _appMetadata = new(); private readonly Mock _appModel = new(); private readonly Mock _instantiationProcessor = new(); private readonly Mock _instantiationValidator = new(); private readonly Mock _pdp = new(); - private readonly Mock _eventsService = new(); + private readonly Mock _eventsService = new(); private readonly IOptions _appSettings = Options.Create(new()); private readonly Mock _prefill = new(); - private readonly Mock _profile = new(); + private readonly Mock _profile = new(); private readonly Mock _processEngine = new(); private readonly Mock _httpContextMock = new(); + private readonly Mock _oarganizationClientMock = new(); private readonly InstancesController SUT; @@ -66,7 +72,8 @@ public InstancesController_CopyInstanceTests() _appSettings, _prefill.Object, _profile.Object, - _processEngine.Object) + _processEngine.Object, + _oarganizationClientMock.Object) { ControllerContext = controllerContext }; diff --git a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs index 88bbb9076..95cee11c6 100644 --- a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs @@ -1,14 +1,14 @@ -using System.Collections.Generic; using System.Net.Http.Headers; using System.Security.Claims; using Altinn.App.Api.Controllers; using Altinn.App.Api.Tests.Controllers.TestResources; using Altinn.App.Api.Tests.Utils; using Altinn.App.Core.Features; -using Altinn.App.Core.Features.DataProcessing; -using Altinn.App.Core.Infrastructure.Clients.Profile; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Prefill; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.Authorization.ABAC.Xacml; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Interfaces; @@ -36,7 +36,7 @@ public async void Get_Returns_BadRequest_when_dataType_is_null() var appResourcesMock = new Mock(); var dataProcessorMock = new Mock(); var prefillMock = new Mock(); - var registerMock = new Mock(); + var registerMock = new Mock(); var pdpMock = new Mock(); ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, altinnAppModelMock.Object, appResourcesMock.Object, @@ -65,7 +65,7 @@ public async void Get_Returns_BadRequest_when_appResource_classRef_is_null() var appResourcesMock = new Mock(); var dataProcessorMock = new Mock(); var prefillMock = new Mock(); - var registerMock = new Mock(); + var registerMock = new Mock(); var pdpMock = new Mock(); var dataType = "some-value"; ILogger logger = new NullLogger(); @@ -92,8 +92,8 @@ public async void Get_Returns_BadRequest_when_appResource_classRef_is_null() // party headers. private class StatelessDataControllerWebApplicationFactory : WebApplicationFactory { - public Mock ProfileClientMoq { get; set; } = new(); - public Mock RegisterClientMoq { get; set; } = new(); + public Mock ProfileClientMoq { get; set; } = new(); + public Mock RegisterClientMoq { get; set; } = new(); public Mock AppResourcesMoq { get; set; } = new(); protected override void ConfigureWebHost(IWebHostBuilder builder) @@ -102,8 +102,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder.ConfigureServices(services=> { - services.AddTransient((sp)=>ProfileClientMoq.Object); - services.AddTransient((sp)=>RegisterClientMoq.Object); + services.AddTransient((sp)=>ProfileClientMoq.Object); + services.AddTransient((sp)=>RegisterClientMoq.Object); services.AddTransient((sp)=>AppResourcesMoq.Object); }); } @@ -170,7 +170,7 @@ public async void Get_Returns_BadRequest_when_instance_owner_is_empty_party_head var appResourcesMock = new Mock(); var dataProcessorMock = new Mock(); var prefillMock = new Mock(); - var registerMock = new Mock(); + var registerMock = new Mock(); var pdpMock = new Mock(); var dataType = "some-value"; ILogger logger = new NullLogger(); @@ -201,7 +201,7 @@ public async void Get_Returns_BadRequest_when_instance_owner_is_empty_user_in_co var appResourcesMock = new Mock(); var dataProcessorMock = new Mock(); var prefillMock = new Mock(); - var registerMock = new Mock(); + var registerMock = new Mock(); var pdpMock = new Mock(); var dataType = "some-value"; ILogger logger = new NullLogger(); @@ -241,7 +241,7 @@ public async void Get_Returns_Forbidden_when_returned_descision_is_Deny() var appResourcesMock = new Mock(); var dataProcessorMock = new Mock(); var prefillMock = new Mock(); - var registerMock = new Mock(); + var registerMock = new Mock(); var pdpMock = new Mock(); var dataType = "some-value"; ILogger logger = new NullLogger(); @@ -298,7 +298,7 @@ public async void Get_Returns_OK_with_appModel() var appResourcesMock = new Mock(); var dataProcessorMock = new Mock(); var prefillMock = new Mock(); - var registerMock = new Mock(); + var registerMock = new Mock(); var pdpMock = new Mock(); var dataType = "some-value"; var classRef = typeof(DummyModel).FullName!; diff --git a/test/Altinn.App.Api.Tests/Controllers/StatelessPagesControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/StatelessPagesControllerTests.cs index c4d8ff8eb..b62d67387 100644 --- a/test/Altinn.App.Api.Tests/Controllers/StatelessPagesControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/StatelessPagesControllerTests.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Altinn.App.Api.Controllers; using Altinn.App.Api.Tests.Controllers.TestResources; using Altinn.App.Core.Features; -using Altinn.App.Core.Features.PageOrder; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Models; using FluentAssertions; diff --git a/test/Altinn.App.Api.Tests/Controllers/TextsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/TextsControllerTests.cs index 59fff3a09..01d5482d7 100644 --- a/test/Altinn.App.Api.Tests/Controllers/TextsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/TextsControllerTests.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; using Altinn.App.Api.Controllers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.AspNetCore.Mvc; diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs index 5a5074f5d..b29cb84b8 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs @@ -1,7 +1,8 @@ using Altinn.App.Api.Controllers; using Altinn.App.Core.Features.Validation; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Infrastructure.Clients; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; @@ -17,7 +18,7 @@ public class ValidateControllerTests public async Task ValidateInstance_returns_NotFound_when_GetInstance_returns_null() { // Arrange - var instanceMock = new Mock(); + var instanceMock = new Mock(); var appMetadataMock = new Mock(); var validationMock = new Mock(); @@ -41,7 +42,7 @@ public async Task ValidateInstance_returns_NotFound_when_GetInstance_returns_nul public async Task ValidateInstance_throws_ValidationException_when_Instance_Process_is_null() { // Arrange - var instanceMock = new Mock(); + var instanceMock = new Mock(); var appMetadataMock = new Mock(); var validationMock = new Mock(); @@ -73,7 +74,7 @@ public async Task ValidateInstance_throws_ValidationException_when_Instance_Proc public async Task ValidateInstance_throws_ValidationException_when_Instance_Process_CurrentTask_is_null() { // Arrange - var instanceMock = new Mock(); + var instanceMock = new Mock(); var appMetadataMock = new Mock(); var validationMock = new Mock(); @@ -108,7 +109,7 @@ public async Task ValidateInstance_throws_ValidationException_when_Instance_Proc public async Task ValidateInstance_returns_OK_with_messages() { // Arrange - var instanceMock = new Mock(); + var instanceMock = new Mock(); var appMetadataMock = new Mock(); var validationMock = new Mock(); diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs index 2e6035a1c..3cb418a19 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs @@ -2,8 +2,9 @@ using Altinn.App.Api.Controllers; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Infrastructure.Clients; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; @@ -234,16 +235,16 @@ public async Task TestValidateData(ValidateDataTestScenario testScenario) private static ValidateController SetupController(string app, string org, int instanceOwnerId, ValidateDataTestScenario testScenario) { - (Mock instanceMock, Mock appResourceMock, Mock validationMock) = + (Mock instanceMock, Mock appResourceMock, Mock validationMock) = SetupMocks(app, org, instanceOwnerId, testScenario); return new ValidateController(instanceMock.Object, validationMock.Object, appResourceMock.Object); } - private static (Mock, Mock, Mock) SetupMocks(string app, string org, + private static (Mock, Mock, Mock) SetupMocks(string app, string org, int instanceOwnerId, ValidateDataTestScenario testScenario) { - var instanceMock = new Mock(); + var instanceMock = new Mock(); var appMetadataMock = new Mock(); var validationMock = new Mock(); if (testScenario.ReceivedInstance != null) diff --git a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs index 4cb2638aa..39ed807f8 100644 --- a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs @@ -1,14 +1,11 @@ -using Altinn.App.Core.Interface; -using Altinn.Platform.Register.Models; -using System; -using System.Collections.Generic; +using Altinn.Platform.Register.Models; using System.Security.Claims; -using System.Threading.Tasks; +using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Models; namespace Altinn.App.Api.Tests.Mocks { - public class AuthorizationMock : IAuthorization + public class AuthorizationMock : IAuthorizationClient { public Task?> GetPartyList(int userId) { diff --git a/test/Altinn.App.Api.Tests/Mocks/InstanceMockSI.cs b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs similarity index 98% rename from test/Altinn.App.Api.Tests/Mocks/InstanceMockSI.cs rename to test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs index 9ced445b7..e4d8c0ede 100644 --- a/test/Altinn.App.Api.Tests/Mocks/InstanceMockSI.cs +++ b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs @@ -1,27 +1,21 @@ using Altinn.App.Api.Tests.Data; using Altinn.App.Core.Extensions; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; +using Altinn.App.Core.Internal.Instances; namespace Altinn.App.Api.Tests.Mocks { - public class InstanceMockSI : IInstance + public class InstanceClientMockSi : IInstanceClient { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; - public InstanceMockSI(ILogger logger, IHttpContextAccessor httpContextAccessor) + public InstanceClientMockSi(ILogger logger, IHttpContextAccessor httpContextAccessor) { _logger = logger; _httpContextAccessor = httpContextAccessor; diff --git a/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs b/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs index 18fe1f030..a4ad04ad2 100644 --- a/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs +++ b/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs @@ -1,5 +1,4 @@ -using Altinn.App.Core.Interface; -using Altinn.Authorization.ABAC.Constants; +using Altinn.Authorization.ABAC.Constants; using Altinn.Authorization.ABAC.Utils; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Authorization.ABAC.Xacml; @@ -9,22 +8,18 @@ using Authorization.Platform.Authorization.Models; using Microsoft.Extensions.Options; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Security.Claims; -using System.Threading.Tasks; using System.Xml; using Altinn.App.Api.Tests.Models; using Altinn.App.Api.Tests.Constants; using Altinn.App.Api.Tests.Data; +using Altinn.App.Core.Internal.Instances; namespace Altinn.App.Api.Tests.Mocks { public class PepWithPDPAuthorizationMockSI : Common.PEP.Interfaces.IPDP { - private readonly IInstance _instanceService; + private readonly IInstanceClient _instanceClient; private readonly PepSettings _pepSettings; @@ -44,9 +39,9 @@ public class PepWithPDPAuthorizationMockSI : Common.PEP.Interfaces.IPDP private const string AltinnRoleAttributeId = "urn:altinn:rolecode"; - public PepWithPDPAuthorizationMockSI(IInstance instanceService, IOptions pepSettings) + public PepWithPDPAuthorizationMockSI(IInstanceClient instanceClient, IOptions pepSettings) { - this._instanceService = instanceService; + this._instanceClient = instanceClient; _pepSettings = pepSettings.Value; } @@ -121,7 +116,7 @@ private async Task EnrichResourceAttributes(XacmlContextRequest request) if (!resourceAttributeComplete) { - Instance instanceData = await _instanceService.GetInstance(resourceAttributes.AppValue, resourceAttributes.OrgValue, Convert.ToInt32(resourceAttributes.InstanceValue.Split('/')[0]), new Guid(resourceAttributes.InstanceValue.Split('/')[1])); + Instance instanceData = await _instanceClient.GetInstance(resourceAttributes.AppValue, resourceAttributes.OrgValue, Convert.ToInt32(resourceAttributes.InstanceValue.Split('/')[0]), new Guid(resourceAttributes.InstanceValue.Split('/')[1])); if (instanceData != null) { diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs index be481c12a..b28c7ccac 100644 --- a/test/Altinn.App.Api.Tests/Program.cs +++ b/test/Altinn.App.Api.Tests/Program.cs @@ -4,11 +4,11 @@ using Altinn.App.Api.Tests.Mocks.Event; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Instances; using AltinnCore.Authentication.JwtCookie; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -38,8 +38,8 @@ void ConfigureMockServices(IServiceCollection services, ConfigurationManager con { PlatformSettings platformSettings = new PlatformSettings() { ApiAuthorizationEndpoint = "http://localhost:5101/authorization/api/v1/" }; services.AddSingleton>(Options.Create(platformSettings)); - services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton, JwtCookiePostConfigureOptionsStub>(); diff --git a/test/Altinn.App.Api.Tests/TestResources/Altinn.App.Api.xml b/test/Altinn.App.Api.Tests/TestResources/Altinn.App.Api.xml index e10f6ce62..ffadad03c 100644 --- a/test/Altinn.App.Api.Tests/TestResources/Altinn.App.Api.xml +++ b/test/Altinn.App.Api.Tests/TestResources/Altinn.App.Api.xml @@ -103,7 +103,7 @@ Exposes API endpoints related to authentication. - + Initializes a new instance of the class @@ -125,7 +125,7 @@ Exposes API endpoints related to authorization - + Initializes a new instance of the class @@ -149,7 +149,7 @@ The data controller handles creation, update, validation and calculation of data elements. - + The data controller is responsible for adding business logic to the data elements. @@ -222,7 +222,7 @@ This controller class provides action methods for endpoints related to the tags resource on data elements. - + Initialize a new instance of with the given services. @@ -309,7 +309,7 @@ You can create a new instance (POST), update it (PUT) and retrieve a specific instance (GET). - + Initializes a new instance of the class @@ -455,7 +455,7 @@ Handles party related operations - + Initializes a new instance of the class @@ -489,7 +489,7 @@ Controller for setting and moving process flow of an instance. - + Initializes a new instance of the @@ -560,7 +560,7 @@ Controller that exposes profile - + Initializes a new instance of the class @@ -702,7 +702,7 @@ The stateless data controller handles creation and calculation of data elements not related to an instance. - + The stateless data controller is responsible for creating and updating stateles data elements. @@ -783,7 +783,7 @@ Represents all actions related to validation of data and instances - + Initialises a new instance of the class diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index 653f6b5dd..9051197e7 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -71,6 +71,9 @@ Always + + Always + Always diff --git a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs new file mode 100644 index 000000000..d2bdbe46b --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs @@ -0,0 +1,97 @@ +using System.Text.Json; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Sign; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.UserAction; +using Altinn.App.Core.Tests.Internal.Process.TestUtils; +using Altinn.Platform.Profile.Models; +using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; +using Signee = Altinn.App.Core.Internal.Sign.Signee; + +namespace Altinn.App.Core.Tests.Features.Action; + +public class SigningUserActionTests +{ + [Fact] + public async void HandleAction_returns_ok_if_user_is_valid() + { + // Arrange + UserProfile userProfile = new UserProfile() + { + UserId = 1337, + Party = new Party() { SSN = "12345678901" } + }; + (var userAction, var signClientMock)= CreateSigningUserAction(userProfile); + var instance = new Instance() + { + Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", + InstanceOwner = new() + { + PartyId = "5000", + }, + Process = new() + { + CurrentTask = new() + { + ElementId = "Task2" + } + }, + Data = new() + { + new() + { + Id = "a499c3ef-e88a-436b-8650-1c43e5037ada", + DataType = "Model" + } + } + }; + var userActionContext = new UserActionContext(instance, 1337); + + // Act + var result = await userAction.HandleAction(userActionContext); + + // Assert + SignatureContext expected = new SignatureContext(new InstanceIdentifier(instance), "signature", new Signee() { UserId = "1337", PersonNumber = "12345678901" }, new DataElementSignature("a499c3ef-e88a-436b-8650-1c43e5037ada")); + signClientMock.Verify(s => s.SignDataElements(It.Is(sc => AssertSigningContextAsExpected(sc, expected))), Times.Once); + result.Should().BeTrue(); + } + + private (SigningUserAction SigningUserAction, Mock SignClientMock) CreateSigningUserAction(UserProfile userProfileToReturn = null, PlatformHttpException platformHttpExceptionToThrow = null) + { + IProcessReader processReader = ProcessTestUtils.SetupProcessReader("signing-task-process.bpmn", Path.Combine("Features", "Action", "TestData")); + AppSettings appSettings = new AppSettings() + { + AppBasePath = Path.Combine("Features", "Action"), + ConfigurationFolder = "TestData", + ApplicationMetadataFileName = "appmetadata.json" + }; + + IAppMetadata appMetadata = new AppMetadata(Options.Create(appSettings), new FrontendFeatures()); + var profileClientMock = new Mock(); + var signingClientMock = new Mock(); + profileClientMock.Setup(p => p.GetUserProfile(It.IsAny())).ReturnsAsync(userProfileToReturn); + if (platformHttpExceptionToThrow != null) + { + signingClientMock.Setup(p => p.SignDataElements(It.IsAny())).ThrowsAsync(platformHttpExceptionToThrow); + } + + return (new SigningUserAction(processReader, new NullLogger(), appMetadata, profileClientMock.Object, signingClientMock.Object), signingClientMock); + } + + private bool AssertSigningContextAsExpected(SignatureContext s1, SignatureContext s2) + { + s1.Should().BeEquivalentTo(s2); + return true; + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/appmetadata.json b/test/Altinn.App.Core.Tests/Features/Action/TestData/appmetadata.json new file mode 100644 index 000000000..51330e670 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/appmetadata.json @@ -0,0 +1,72 @@ +{ + "id": "ttd/vga-dev-v8", + "org": "ttd", + "title": { + "nb": "vga-dev-v8", + "en": "vga-dev-v8" + }, + "dataTypes": [ + { + "id": "ref-data-as-pdf", + "allowedContentTypes": [ + "application/pdf" + ], + "maxCount": 0, + "minCount": 0, + "enablePdfCreation": true + }, + { + "id": "Model", + "allowedContentTypes": [ + "application/xml" + ], + "appLogic": { + "autoCreate": true, + "classRef": "Altinn.App.Models.Model", + "allowAnonymousOnStateless": false, + "autoDeleteOnProcessEnd": false + }, + "taskId": "Task_1", + "maxCount": 1, + "minCount": 1, + "enablePdfCreation": true + }, + { + "id": "Extra", + "allowedContentTypes": [ + "application/xml" + ], + "appLogic": { + "autoCreate": true, + "classRef": "Altinn.App.Models.Extra", + "allowAnonymousOnStateless": false, + "autoDeleteOnProcessEnd": false + }, + "taskId": "Task_3", + "maxCount": 1, + "minCount": 1, + "enablePdfCreation": true + }, + { + "id": "signature", + "allowedContentTypes": [ + "application/json" + ], + "taskId": "Task_2", + "maxCount": 1, + "minCount": 0, + "enablePdfCreation": false + } + ], + "partyTypesAllowed": { + "bankruptcyEstate": false, + "organisation": false, + "person": false, + "subUnit": false + }, + "autoDeleteOnProcessEnd": false, + "created": "2022-10-21T07:30:47.2710111Z", + "createdBy": "tjololo", + "lastChanged": "2022-10-21T07:30:47.2710121Z", + "lastChangedBy": "tjololo" +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process.bpmn b/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process.bpmn new file mode 100644 index 000000000..024c830ee --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process.bpmn @@ -0,0 +1,49 @@ + + + + + Flow1 + + + + Flow1 + Flow2 + + + + + + data + + + + + + Flow2 + Flow3 + + + + + + + signing + + Model + + signature + + + + + + Flow3 + + + diff --git a/test/Altinn.App.Core.Tests/Features/Action/UserActionFactoryTests.cs b/test/Altinn.App.Core.Tests/Features/Action/UserActionFactoryTests.cs new file mode 100644 index 000000000..af038b845 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/UserActionFactoryTests.cs @@ -0,0 +1,74 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Models.UserAction; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Action; + +public class UserActionFactoryTests +{ + [Fact] + public void GetActionHandler_should_return_DummyActionHandler_for_id_dummy() + { + var factory = new UserActionFactory(new List() { new DummyUserAction() }); + + IUserAction userAction = factory.GetActionHandler("dummy"); + + userAction.Should().BeOfType(); + userAction.Id.Should().Be("dummy"); + } + + [Fact] + public void GetActionHandler_should_return_first_DummyActionHandler_for_id_dummy_if_multiple() + { + var factory = new UserActionFactory(new List() { new DummyUserAction(), new DummyUserAction2() }); + + IUserAction userAction = factory.GetActionHandler("dummy"); + + userAction.Should().BeOfType(); + userAction.Id.Should().Be("dummy"); + } + + [Fact] + public void GetActionHandler_should_return_NullActionHandler_if_id_not_found() + { + var factory = new UserActionFactory(new List() { new DummyUserAction() }); + + IUserAction userAction = factory.GetActionHandler("nonexisting"); + + userAction.Should().BeOfType(); + userAction.Id.Should().Be("null"); + } + + [Fact] + public void GetActionHandler_should_return_NullActionHandler_if_id_is_null() + { + var factory = new UserActionFactory(new List() { new DummyUserAction() }); + + IUserAction userAction = factory.GetActionHandler(null); + + userAction.Should().BeOfType(); + userAction.Id.Should().Be("null"); + } + + internal class DummyUserAction : IUserAction + { + public string Id { get; set; } = "dummy"; + + public Task HandleAction(UserActionContext context) + { + return Task.FromResult(true); + } + } + + internal class DummyUserAction2 : IUserAction + { + public string Id { get; set; } = "dummy"; + + public Task HandleAction(UserActionContext context) + { + return Task.FromResult(true); + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs b/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs index dc1fa0443..bac0790aa 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs @@ -2,10 +2,11 @@ using System.Text.Json.Serialization; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Validation; -using Altinn.App.Core.Interface; 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.Models.Validation; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; @@ -93,8 +94,8 @@ public async Task FileScanEnabled_Clean_ValidationShouldNotFail() private static ValidationAppSI ConfigureMockServicesForValidation() { Mock> loggerMock = new(); - var dataMock = new Mock(); - var instanceMock = new Mock(); + var dataMock = new Mock(); + var instanceMock = new Mock(); var instanceValidator = new Mock(); var appModelMock = new Mock(); var appResourcesMock = new Mock(); diff --git a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs index 91b4ce974..e3066e229 100644 --- a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs @@ -1,6 +1,5 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Implementation; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; diff --git a/test/Altinn.App.Core.Tests/Implementation/DefaultPageOrderTest.cs b/test/Altinn.App.Core.Tests/Implementation/DefaultPageOrderTest.cs index 571daa7d0..6fb626122 100644 --- a/test/Altinn.App.Core.Tests/Implementation/DefaultPageOrderTest.cs +++ b/test/Altinn.App.Core.Tests/Implementation/DefaultPageOrderTest.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Altinn.App.Core.Features.PageOrder; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Moq; using Xunit; diff --git a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs b/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs index db5e0d72f..9a9b9868d 100644 --- a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs @@ -1,13 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Altinn.App.Core.Features; using Altinn.App.Core.Implementation; -using Altinn.App.Core.Interface; 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.Models; @@ -25,12 +24,12 @@ public class DefaultTaskEventsTests: IDisposable private readonly Mock _resMock; private readonly Mock _metaMock; private readonly ApplicationMetadata _application; - private readonly Mock _dataMock; + 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 readonly Mock _instanceMock; private IEnumerable _taskStarts; private IEnumerable _taskEnds; private IEnumerable _taskAbandons; @@ -43,12 +42,12 @@ public DefaultTaskEventsTests() _application = new ApplicationMetadata("ttd/test"); _resMock = new Mock(); _metaMock = new Mock(); - _dataMock = new Mock(); + _dataMock = new Mock(); _prefillMock = new Mock(); _appModel = new DefaultAppModel(NullLogger.Instance); _appModelMock = new Mock(); _instantiationMock = new Mock(); - _instanceMock = new Mock(); + _instanceMock = new Mock(); _taskStarts = new List(); _taskEnds = new List(); _taskAbandons = new List(); diff --git a/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs b/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs index f2fa4c501..71861bbe1 100644 --- a/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/PersonClientTests.cs @@ -5,8 +5,8 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients.Register; -using Altinn.App.Core.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Models; using Altinn.App.PlatformServices.Tests.Mocks; using Altinn.Common.AccessTokenClient.Services; diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/DataClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs similarity index 99% rename from test/Altinn.App.Core.Tests/Infrastructure/Clients/DataClientTests.cs rename to test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs index 6eae354da..612101176 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/DataClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs @@ -6,9 +6,9 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients.Storage; -using Altinn.App.Core.Infrastructure.Clients.Storage.TestData; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Models; +using Altinn.App.Core.Tests.Infrastructure.Clients.Storage.TestData; using Altinn.App.PlatformServices.Tests.Data; using Altinn.App.PlatformServices.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; @@ -19,7 +19,7 @@ using Moq; using Xunit; -namespace Altinn.App.Core.Tests.Infrastructure.Clients +namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage { public class DataClientTests { diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs new file mode 100644 index 000000000..81a80bb9b --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs @@ -0,0 +1,139 @@ +#nullable enable +using System.Net; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Infrastructure.Clients.Storage; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Sign; +using Altinn.App.Core.Models; +using Altinn.App.PlatformServices.Tests.Mocks; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; +using Signee = Altinn.App.Core.Internal.Sign.Signee; + +namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage; + +public class SignClientTests +{ + private readonly IOptions platformSettingsOptions; + private readonly Mock userTokenProvide; + private readonly string apiStorageEndpoint = "https://local.platform.altinn.no/api/storage/"; + + public SignClientTests() + { + platformSettingsOptions = Options.Create(new PlatformSettings() + { + ApiStorageEndpoint = apiStorageEndpoint, + SubscriptionKey = "test" + }); + + userTokenProvide = new Mock(); + userTokenProvide.Setup(s => s.GetUserToken()).Returns("dummytoken"); + } + + [Fact] + public async Task SignDataElements_sends_request_to_platform() + { + // Arrange + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(1337, Guid.NewGuid()); + HttpRequestMessage? platformRequest = null; + int callCount = 0; + SignClient signClient = GetSignClient((request, token) => + { + callCount++; + platformRequest = request; + return Task.FromResult(new HttpResponseMessage + { + StatusCode = HttpStatusCode.Created + }); + }); + + // Act + var dataElementId = Guid.NewGuid().ToString(); + var signatureContext = new SignatureContext( + instanceIdentifier, + "sign-data-type", + new Signee() + { + UserId = "1337", + PersonNumber = "0101011337" + }, + new DataElementSignature(dataElementId)); + + SignRequest expectedRequest = new SignRequest() + { + Signee = new() + { + UserId = "1337", + PersonNumber = "0101011337" + }, + DataElementSignatures = new() + { + new() + { + DataElementId = dataElementId, + Signed = true + } + }, + SignatureDocumentDataType = "sign-data-type" + }; + + await signClient.SignDataElements(signatureContext); + + // Assert + userTokenProvide.Verify(s => s.GetUserToken(), Times.Once); + callCount.Should().Be(1); + platformRequest.Should().NotBeNull(); + platformRequest!.Method.Should().Be(HttpMethod.Post); + platformRequest!.RequestUri!.ToString().Should().Be($"{apiStorageEndpoint}instances/{instanceIdentifier.InstanceOwnerPartyId}/{instanceIdentifier.InstanceGuid}/sign"); + SignRequest actual = await platformRequest.Content.ReadAsAsync(); + actual.Should().BeEquivalentTo(expectedRequest); + } + + [Fact] + public async Task SignDataElements_throws_PlatformHttpException_if_platform_returns_http_errorcode() + { + // Arrange + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(1337, Guid.NewGuid()); + HttpRequestMessage? platformRequest = null; + int callCount = 0; + SignClient signClient = GetSignClient((request, token) => + { + callCount++; + platformRequest = request; + return Task.FromResult(new HttpResponseMessage + { + StatusCode = HttpStatusCode.InternalServerError + }); + }); + + // Act + var dataElementId = Guid.NewGuid().ToString(); + var signatureContext = new SignatureContext( + instanceIdentifier, + "sign-data-type", + new Signee() + { + UserId = "1337", + PersonNumber = "0101011337" + }, + new DataElementSignature(dataElementId)); + + var ex = await Assert.ThrowsAsync(async() => await signClient.SignDataElements(signatureContext)); + ex.Should().NotBeNull(); + ex.Response.Should().NotBeNull(); + ex.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + + private SignClient GetSignClient(Func> handlerFunc) + { + DelegatingHandlerStub delegatingHandlerStub = new(handlerFunc); + return new SignClient( + platformSettingsOptions, + new HttpClient(delegatingHandlerStub), + userTokenProvide.Object); + } +} diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TestData/ExampleModel.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/TestData/ExampleModel.cs similarity index 64% rename from src/Altinn.App.Core/Infrastructure/Clients/Storage/TestData/ExampleModel.cs rename to test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/TestData/ExampleModel.cs index 7643dbf51..5f22ebefb 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TestData/ExampleModel.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/TestData/ExampleModel.cs @@ -1,4 +1,4 @@ -namespace Altinn.App.Core.Infrastructure.Clients.Storage.TestData; +namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage.TestData; /// /// Example Model used in tests @@ -8,7 +8,8 @@ public class ExampleModel /// /// The name /// - public string Name { get; set; } = ""; + public string Name { get; set; } = string.Empty; + /// /// The age /// diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index f5b45b7b0..7d59a359f 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -4,8 +4,12 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; using Altinn.App.Core.Infrastructure.Clients.Pdf; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Pdf; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using Altinn.App.PlatformServices.Tests.Helpers; using Altinn.App.PlatformServices.Tests.Mocks; using Altinn.Platform.Storage.Interface.Models; @@ -24,11 +28,11 @@ public class PdfServiceTests private readonly Mock _pdf = new(); private readonly Mock _appResources = new(); private readonly Mock _pdfOptionsMapping = new(); - private readonly Mock _dataClient = new(); + private readonly Mock _dataClient = new(); private readonly Mock _httpContextAccessor = new(); private readonly Mock _pdfGeneratorClient = new(); - private readonly Mock _profile = new(); - private readonly Mock _register = new(); + private readonly Mock _profile = new(); + private readonly Mock _register = new(); private readonly Mock pdfFormatter = new(); private readonly IOptions _pdfGeneratorSettingsOptions = Microsoft.Extensions.Options.Options.Create(new() { }); diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs index 9e1ae4ccd..87c4cd8df 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs @@ -2,9 +2,9 @@ using System.Text.Json; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; -using Altinn.App.Core.Interface; 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; using Altinn.App.Core.Internal.Process.Elements; @@ -343,7 +343,7 @@ private static ExpressionsExclusiveGateway SetupExpressionsGateway(List(); var appModel = new Mock(); var appMetadata = new Mock(); - var dataClient = new Mock(); + var dataClient = new Mock(); resources.Setup(r => r.GetLayoutSets()).Returns(layoutSets ?? string.Empty); appMetadata.Setup(m => m.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata("ttd/test-app") diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index 7fa228550..f06e4d029 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -1,9 +1,11 @@ #nullable enable using System.Security.Claims; using Altinn.App.Core.Extensions; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Action; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Models.Process; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; @@ -20,7 +22,7 @@ namespace Altinn.App.Core.Tests.Internal.Process; public class ProcessEngineTest : IDisposable { private Mock _processReaderMock; - private readonly Mock _profileMock; + private readonly Mock _profileMock; private readonly Mock _processNavigatorMock; private readonly Mock _processEventDispatcherMock; @@ -822,7 +824,7 @@ private IProcessEngine GetProcessEngine(Mock? processReaderMock Name = "Utfylling", ExtensionElements = new() { - AltinnProperties = new() + TaskExtension = new() { TaskType = "data" } @@ -838,7 +840,7 @@ private IProcessEngine GetProcessEngine(Mock? processReaderMock Name = "Bekreft", ExtensionElements = new() { - AltinnProperties = new() + TaskExtension = new() { TaskType = "confirmation" } @@ -861,7 +863,8 @@ private IProcessEngine GetProcessEngine(Mock? processReaderMock _processReaderMock.Object, _profileMock.Object, _processNavigatorMock.Object, - _processEventDispatcherMock.Object); + _processEventDispatcherMock.Object, + new UserActionFactory(new List())); } public void Dispose() diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs index db1856666..43b19640f 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs @@ -1,5 +1,7 @@ using Altinn.App.Core.Configuration; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Process; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; @@ -17,11 +19,11 @@ public class ProcessEventDispatcherTests 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 instanceService = new Mock(); + var instanceEvent = new Mock(); var taskEvents = new Mock(); var appEvents = new Mock(); - var eventsService = new Mock(); + var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( @@ -104,11 +106,11 @@ public async Task UpdateProcessAndDispatchEvents_StartEvent_instance_updated_and public async Task UpdateProcessAndDispatchEvents_StartTask_instance_updated_and_events_sent_to_storage_nothing_sent_to_ITask_when_tasktype_missing() { // Arrange - var instanceService = new Mock(); - var instanceEvent = new Mock(); + var instanceService = new Mock(); + var instanceEvent = new Mock(); var taskEvents = new Mock(); var appEvents = new Mock(); - var eventsService = new Mock(); + var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( @@ -188,11 +190,11 @@ public async Task UpdateProcessAndDispatchEvents_StartTask_instance_updated_and_ public async Task UpdateProcessAndDispatchEvents_StartTask_data_instance_updated_and_events_sent_to_storage_and_trigger_ITask() { // Arrange - var instanceService = new Mock(); - var instanceEvent = new Mock(); + var instanceService = new Mock(); + var instanceEvent = new Mock(); var taskEvents = new Mock(); var appEvents = new Mock(); - var eventsService = new Mock(); + var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( @@ -277,11 +279,11 @@ public async Task UpdateProcessAndDispatchEvents_StartTask_data_instance_updated public async Task UpdateProcessAndDispatchEvents_EndTask_confirmation_instance_updated_and_events_sent_to_storage_and_trigger_ITask() { // Arrange - var instanceService = new Mock(); - var instanceEvent = new Mock(); + var instanceService = new Mock(); + var instanceEvent = new Mock(); var taskEvents = new Mock(); var appEvents = new Mock(); - var eventsService = new Mock(); + var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( @@ -366,11 +368,11 @@ public async Task UpdateProcessAndDispatchEvents_EndTask_confirmation_instance_u public async Task UpdateProcessAndDispatchEvents_AbandonTask_feedback_instance_updated_and_events_sent_to_storage_and_trigger_ITask() { // Arrange - var instanceService = new Mock(); - var instanceEvent = new Mock(); + var instanceService = new Mock(); + var instanceEvent = new Mock(); var taskEvents = new Mock(); var appEvents = new Mock(); - var eventsService = new Mock(); + var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( @@ -455,11 +457,11 @@ public async Task UpdateProcessAndDispatchEvents_AbandonTask_feedback_instance_u public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_updated_and_events_sent_to_storage_and_trigger_ITask() { // Arrange - var instanceService = new Mock(); - var instanceEvent = new Mock(); + var instanceService = new Mock(); + var instanceEvent = new Mock(); var taskEvents = new Mock(); var appEvents = new Mock(); - var eventsService = new Mock(); + var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( @@ -543,11 +545,11 @@ public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_ public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_updated_and_dispatches_no_events_when_events_null() { // Arrange - var instanceService = new Mock(); - var instanceEvent = new Mock(); + var instanceService = new Mock(); + var instanceEvent = new Mock(); var taskEvents = new Mock(); var appEvents = new Mock(); - var eventsService = new Mock(); + var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings()); var logger = new NullLogger(); IProcessEventDispatcher dispatcher = new ProcessEventDispatcher( @@ -612,11 +614,11 @@ public async Task UpdateProcessAndDispatchEvents_EndEvent_confirmation_instance_ public async Task RegisterEventWithEventsComponent_sends_movedTo_event_to_events_system_when_enabled_and_current_task_set() { // Arrange - var instanceService = new Mock(); - var instanceEvent = new Mock(); + var instanceService = new Mock(); + var instanceEvent = new Mock(); var taskEvents = new Mock(); var appEvents = new Mock(); - var eventsService = new Mock(); + var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings() { RegisterEventsWithEventsComponent = true @@ -658,11 +660,11 @@ public async Task RegisterEventWithEventsComponent_sends_movedTo_event_to_events public async Task RegisterEventWithEventsComponent_sends_complete_event_to_events_system_when_currentTask_null_and_endevent_set() { // Arrange - var instanceService = new Mock(); - var instanceEvent = new Mock(); + var instanceService = new Mock(); + var instanceEvent = new Mock(); var taskEvents = new Mock(); var appEvents = new Mock(); - var eventsService = new Mock(); + var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings() { RegisterEventsWithEventsComponent = true @@ -702,11 +704,11 @@ public async Task RegisterEventWithEventsComponent_sends_complete_event_to_event public async Task RegisterEventWithEventsComponent_sends_no_events_when_process_is_null() { // Arrange - var instanceService = new Mock(); - var instanceEvent = new Mock(); + var instanceService = new Mock(); + var instanceEvent = new Mock(); var taskEvents = new Mock(); var appEvents = new Mock(); - var eventsService = new Mock(); + var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings() { RegisterEventsWithEventsComponent = true @@ -741,11 +743,11 @@ public async Task RegisterEventWithEventsComponent_sends_no_events_when_process_ public async Task RegisterEventWithEventsComponent_sends_no_events_when_current_and_endevent_is_null() { // Arrange - var instanceService = new Mock(); - var instanceEvent = new Mock(); + var instanceService = new Mock(); + var instanceEvent = new Mock(); var taskEvents = new Mock(); var appEvents = new Mock(); - var eventsService = new Mock(); + var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings() { RegisterEventsWithEventsComponent = true @@ -780,11 +782,11 @@ public async Task RegisterEventWithEventsComponent_sends_no_events_when_current_ public async Task RegisterEventWithEventsComponent_sends_no_events_when_registereventswitheventscomponent_false() { // Arrange - var instanceService = new Mock(); - var instanceEvent = new Mock(); + var instanceService = new Mock(); + var instanceEvent = new Mock(); var taskEvents = new Mock(); var appEvents = new Mock(); - var eventsService = new Mock(); + var eventsService = new Mock(); var appSettings = Options.Create(new AppSettings() { RegisterEventsWithEventsComponent = false diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs index f9e150271..f90fe8116 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs @@ -48,7 +48,7 @@ public async void GetNextTask_returns_default_if_no_filtering_is_implemented_and TaskType = null!, ExtensionElements = new() { - AltinnProperties = new() + TaskExtension = new() { TaskType = "confirm", AltinnActions = new() @@ -92,7 +92,7 @@ public async void GetNextTask_runs_custom_filter_and_returns_result() TaskType = null!, ExtensionElements = new() { - AltinnProperties = new() + TaskExtension = new() { TaskType = "data", AltinnActions = new() diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs index e49a3d3ca..17ed16b72 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs @@ -310,7 +310,7 @@ public void GetFlowElement_returns_ProcessTask_with_id() Outgoing = new List { "Flow2" }, ExtensionElements = new ExtensionElements() { - AltinnProperties = new AltinnProperties() + TaskExtension = new AltinnTaskExtension() { AltinnActions = new List() { @@ -319,7 +319,13 @@ public void GetFlowElement_returns_ProcessTask_with_id() Id = "submit", } }, - TaskType = "data" + TaskType = "data", + DataTypesToSign = new() + { + "default", + "default2" + }, + SignatureDataType = "signature" } } }); diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn index a9ad1e9cb..bb23facd4 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn @@ -3,7 +3,7 @@ xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" - xmlns:altinn="http://altinn.no" + xmlns:altinn="http://altinn.no/process" id="Definitions_1eqx4ru" targetNamespace="http://bpmn.io/schema/bpmn" exporter="bpmn-js (https://demo.bpmn.io)" exporterVersion="10.2.0"> @@ -15,12 +15,17 @@ Flow1 Flow2 - + data - + + default + default2 + + signature + @@ -35,13 +40,13 @@ Flow3 Flow5 - + confirm - + diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn index 2c1f15152..d0b809469 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn @@ -3,7 +3,7 @@ xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" - xmlns:altinn="http://altinn.no" + xmlns:altinn="http://altinn.no/process" id="Definitions_1eqx4ru" targetNamespace="http://bpmn.io/schema/bpmn" exporter="bpmn-js (https://demo.bpmn.io)" exporterVersion="10.2.0"> @@ -15,12 +15,12 @@ Flow1 Flow2 - + data - + @@ -35,12 +35,12 @@ Flow3 Flow5 - + data - + diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs b/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs index 041bc7a36..2816d6a40 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestUtils/ProcessTestUtils.cs @@ -1,4 +1,4 @@ -using Altinn.App.Core.Interface; +#nullable enable using Altinn.App.Core.Internal.Process; using Moq; @@ -8,10 +8,15 @@ internal static class ProcessTestUtils { private static readonly string TestDataPath = Path.Combine("Internal", "Process", "TestData"); - internal static ProcessReader SetupProcessReader(string bpmnfile) + internal static ProcessReader SetupProcessReader(string bpmnfile, string? testDataPath = null) { - Mock processServiceMock = new Mock(); - var s = new FileStream(Path.Combine(TestDataPath, bpmnfile), FileMode.Open, FileAccess.Read); + if (testDataPath == null) + { + testDataPath = TestDataPath; + } + + Mock processServiceMock = new Mock(); + var s = new FileStream(Path.Combine(testDataPath, bpmnfile), FileMode.Open, FileAccess.Read); processServiceMock.Setup(p => p.GetProcessDefinition()).Returns(s); return new ProcessReader(processServiceMock.Object); } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/LayoutTestUtils.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/LayoutTestUtils.cs index 0fcebabd6..d1fdd0d61 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/LayoutTestUtils.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/LayoutTestUtils.cs @@ -3,7 +3,8 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Models.Layout; using Altinn.App.Core.Models.Layout.Components; @@ -19,9 +20,9 @@ public static async Task GetLayoutModelTools(object model, { var services = new ServiceCollection(); - var data = new Mock(); + var data = new Mock(); data.Setup(d => d.GetFormData(default, default!, default!, default!, default, default)).ReturnsAsync(model); - services.AddTransient((sp) => data.Object); + services.AddTransient((sp) => data.Object); var resources = new Mock(); var layoutModel = new LayoutModel(); From 95673876de491b2ab8a9a8ad953dc5feeeeb9293 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Tue, 13 Jun 2023 13:14:23 +0200 Subject: [PATCH 04/46] #137 move back without write (#248) * Lock and unlock method in storage client * Mark HttpContent as nullable * reformat code * prepare for deletion of pdfs with outdated data * Fix build error * Last changes after merging in v8 * Remove changes in old obsolete interface * Add generatedFrom logic for pdf generation * Change to supply GeneratedFromTask instead. Remove old GeneratedFrom elements befor new end task is executed * Fix codeQl error, warning and notice * fix nullability warning * Fix codesmell * Fix some small things after self-review * Apply suggestions from code review --- src/Altinn.App.Core/Altinn.App.Core.csproj | 2 +- .../Extensions/HttpClientExtension.cs | 4 +- .../Implementation/DefaultTaskEvents.cs | 35 +++-- .../Clients/Storage/DataClient.cs | 39 ++++- .../Internal/Data/IDataClient.cs | 19 ++- .../Internal/Pdf/IPdfService.cs | 5 +- .../Internal/Pdf/PdfService.cs | 16 +- .../Process/ProcessEventDispatcher.cs | 1 - .../Implementation/DefaultTaskEventsTests.cs | 74 ++++++++- .../Clients/Storage/DataClientTests.cs | 145 +++++++++++++++++- .../Internal/Pdf/PdfServiceTests.cs | 87 ++++++++++- .../Process/ProcessEventDispatcherTests.cs | 2 +- 12 files changed, 386 insertions(+), 43 deletions(-) diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index 66c52689d..7c88662f8 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Altinn.App.Core/Extensions/HttpClientExtension.cs b/src/Altinn.App.Core/Extensions/HttpClientExtension.cs index accfa7989..2b892c84d 100644 --- a/src/Altinn.App.Core/Extensions/HttpClientExtension.cs +++ b/src/Altinn.App.Core/Extensions/HttpClientExtension.cs @@ -14,7 +14,7 @@ public static class HttpClientExtension /// The http content /// The platformAccess tokens /// A HttpResponseMessage - public static Task PostAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent content, string? platformAccessToken = null) + public static Task PostAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent? content, string? platformAccessToken = null) { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); request.Headers.Add("Authorization", "Bearer " + authorizationToken); @@ -36,7 +36,7 @@ public static Task PostAsync(this HttpClient httpClient, st /// The http content /// The platformAccess tokens /// A HttpResponseMessage - public static Task PutAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent content, string? platformAccessToken = null) + public static Task PutAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent? content, string? platformAccessToken = null) { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, requestUri); request.Headers.Add("Authorization", "Bearer " + authorizationToken); diff --git a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs index 75cb9ee81..7aca1d76b 100644 --- a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs +++ b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs @@ -14,6 +14,7 @@ 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; @@ -63,7 +64,7 @@ public DefaultTaskEvents( LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, IOptions? appSettings = null, IEFormidlingService? eFormidlingService = null - ) + ) { _logger = logger; _appResources = appResources; @@ -98,9 +99,8 @@ public async Task OnStartProcessTask(string taskId, Instance instance, Dictionar if (dataElement != null && dataElement.Locked) { - dataElement.Locked = false; _logger.LogDebug("Unlocking data element {DataElementId} of dataType {DataTypeId}", dataElement.Id, dataType.Id); - await _dataClient.Update(instance, dataElement); + await _dataClient.UnlockDataElement(new InstanceIdentifier(instance), Guid.Parse(dataElement.Id)); } } @@ -148,6 +148,8 @@ public async Task OnEndProcessTask(string endEvent, Instance instance) ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata(); List dataTypesToLock = appMetadata.DataTypes.FindAll(dt => dt.TaskId == endEvent); + await RunRemoveDataElementsGeneratedFromTask(instance, endEvent); + await RunRemoveHiddenData(instance, instanceGuid, dataTypesToLock); await RunRemoveShadowFields(instance, instanceGuid, dataTypesToLock); @@ -177,6 +179,16 @@ private async Task RunRemoveShadowFields(Instance instance, Guid instanceGuid, L } } + 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) @@ -195,16 +207,15 @@ private async Task RunLockDataAndGeneratePdf(string endEvent, Instance instance, foreach (DataElement dataElement in instance.Data.FindAll(de => de.DataType == dataType.Id)) { - dataElement.Locked = true; _logger.LogDebug("Locking data element {dataElementId} of dataType {dataTypeId}.", dataElement.Id, dataType.Id); - Task updateData = _dataClient.Update(instance, dataElement); + Task updateData = _dataClient.LockDataElement(new InstanceIdentifier(instance), Guid.Parse(dataElement.Id)); if (generatePdf) { Task createPdf; if (await _featureManager.IsEnabledAsync(FeatureFlags.NewPdfGeneration)) { - createPdf = _pdfService.GenerateAndStorePdf(instance, CancellationToken.None); + createPdf = _pdfService.GenerateAndStorePdf(instance, endEvent, CancellationToken.None); } else { @@ -285,7 +296,7 @@ private async Task RemoveShadowFields(Instance instance, Guid instanceGuid, List instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, dataElementId); var modifier = new IgnorePropertiesWithPrefix(dataType.AppLogic.ShadowFields.Prefix); - JsonSerializerOptions options = new () + JsonSerializerOptions options = new() { TypeInfoResolver = new DefaultJsonTypeInfoResolver { @@ -293,14 +304,16 @@ private async Task RemoveShadowFields(Instance instance, Guid instanceGuid, List }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; - + string serializedData = JsonSerializer.Serialize(data, options); - if (dataType.AppLogic.ShadowFields.SaveToDataType != null) { + if (dataType.AppLogic.ShadowFields.SaveToDataType != null) + { var saveToDataType = dataTypesToLock.Find(dt => dt.Id == dataType.AppLogic.ShadowFields.SaveToDataType); - if (saveToDataType == null) { + 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); diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index c1c834e57..7d4515258 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -20,6 +20,7 @@ using System.Xml; using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Data; +using Microsoft.IdentityModel.Tokens; namespace Altinn.App.Core.Infrastructure.Clients.Storage { @@ -292,9 +293,13 @@ public async Task InsertBinaryData(string org, string app, int inst } /// - public async Task InsertBinaryData(string instanceId, string dataType, string contentType, string filename, Stream stream) + public async Task InsertBinaryData(string instanceId, string dataType, string contentType, string filename, Stream stream, string? generatedFromTask = null) { string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceId}/data?dataType={dataType}"; + if(!string.IsNullOrEmpty(generatedFromTask)) + { + apiUrl += $"&generatedFromTask={generatedFromTask}"; + } string token = _userTokenProvider.GetUserToken(); DataElement dataElement; @@ -401,5 +406,37 @@ public async Task Update(Instance instance, DataElement dataElement throw await PlatformHttpException.CreateAsync(response); } + + /// + public async Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + { + string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock"; + string token = _userTokenProvider.GetUserToken(); + _logger.LogDebug("Locking data element {DataGuid} for instance {InstanceIdentifier} URL: {Url}", dataGuid, instanceIdentifier, apiUrl); + HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content: null); + if (response.IsSuccessStatusCode) + { + DataElement result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync())!; + return result; + } + _logger.LogError("Locking data element {DataGuid} for instance {InstanceIdentifier} failed with status code {StatusCode}", dataGuid, instanceIdentifier, response.StatusCode); + throw await PlatformHttpException.CreateAsync(response); + } + + /// + public async Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + { + string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock"; + string token = _userTokenProvider.GetUserToken(); + _logger.LogDebug("Unlocking data element {DataGuid} for instance {InstanceIdentifier} URL: {Url}", dataGuid, instanceIdentifier, apiUrl); + HttpResponseMessage response = await _client.DeleteAsync(token, apiUrl); + if (response.IsSuccessStatusCode) + { + DataElement result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync())!; + return result; + } + _logger.LogError("Unlocking data element {DataGuid} for instance {InstanceIdentifier} failed with status code {StatusCode}", dataGuid, instanceIdentifier, response.StatusCode); + throw await PlatformHttpException.CreateAsync(response); + } } } diff --git a/src/Altinn.App.Core/Internal/Data/IDataClient.cs b/src/Altinn.App.Core/Internal/Data/IDataClient.cs index aa5a3e9cc..0a727f3fc 100644 --- a/src/Altinn.App.Core/Internal/Data/IDataClient.cs +++ b/src/Altinn.App.Core/Internal/Data/IDataClient.cs @@ -140,8 +140,9 @@ public interface IDataClient /// content type /// filename /// the stream to stream + /// Optional field to set what task the binary data was generated from /// - Task InsertBinaryData(string instanceId, string dataType, string contentType, string filename, Stream stream); + Task InsertBinaryData(string instanceId, string dataType, string contentType, string filename, Stream stream, string? generatedFromTask = null); /// /// Updates the data element metadata object. @@ -150,5 +151,21 @@ public interface IDataClient /// The data element with values to update /// the updated data element Task Update(Instance instance, DataElement dataElement); + + /// + /// Lock data element in storage + /// + /// InstanceIdentifier identifying the instance containing the DataElement to lock + /// Id of the DataElement to lock + /// + Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid); + + /// + /// Unlock data element in storage + /// + /// InstanceIdentifier identifying the instance containing the DataElement to unlock + /// Id of the DataElement to unlock + /// + Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid); } } diff --git a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs index 1a93ea185..3c300aa1a 100644 --- a/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/IPdfService.cs @@ -21,7 +21,8 @@ public interface IPdfService /// to storage as a new binary file associated with the predefined PDF data type in most apps. /// /// The instance details. + /// The task id for witch the pdf is generated /// Cancellation Token for when a request should be stopped before it's completed. - Task GenerateAndStorePdf(Instance instance, CancellationToken ct); + Task GenerateAndStorePdf(Instance instance, string taskId, CancellationToken ct); } -} \ No newline at end of file +} diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index 48813a19f..b49104469 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -67,8 +67,7 @@ public PdfService( IPdfFormatter pdfFormatter, IPdfGeneratorClient pdfGeneratorClient, IOptions pdfGeneratorSettings, - IOptions generalSettings - ) + IOptions generalSettings) { _pdfClient = pdfClient; _resourceService = appResources; @@ -85,7 +84,7 @@ IOptions generalSettings /// - public async Task GenerateAndStorePdf(Instance instance, CancellationToken ct) + public async Task GenerateAndStorePdf(Instance instance, string taskId, CancellationToken ct) { var baseUrl = _generalSettings.FormattedExternalAppBaseUrl(new AppIdentifier(instance)); var pagePath = _pdfGeneratorSettings.AppPdfPagePathTemplate.ToLowerInvariant().Replace("{instanceid}", instance.Id); @@ -102,13 +101,13 @@ public async Task GenerateAndStorePdf(Instance instance, CancellationToken ct) TextResource? textResource = await GetTextResource(appIdentifier.App, appIdentifier.Org, language); string fileName = GetFileName(instance, textResource); - await _dataClient.InsertBinaryData( instance.Id, PdfElementType, PdfContentType, fileName, - pdfContent); + pdfContent, + taskId); } private static Uri BuildUri(string baseUrl, string pagePath, string language) @@ -225,11 +224,11 @@ public async Task GenerateAndStoreReceiptPDF(Instance instance, string taskId, D }; Stream pdfContent = await _pdfClient.GeneratePDF(pdfContext); - await StorePDF(pdfContent, instance, textResource); + await StorePDF(pdfContent, instance, textResource, taskId); pdfContent.Dispose(); } - private async Task StorePDF(Stream pdfStream, Instance instance, TextResource textResource) + private async Task StorePDF(Stream pdfStream, Instance instance, TextResource textResource, string generatedFromTask) { string? fileName = null; string app = instance.AppId.Split("/")[1]; @@ -247,7 +246,8 @@ private async Task StorePDF(Stream pdfStream, Instance instance, Te PdfElementType, PdfContentType, fileName, - pdfStream); + pdfStream, + generatedFromTask); } private async Task GetLanguage() diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs b/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs index 25dabace8..f87944693 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEventDispatcher.cs @@ -122,7 +122,6 @@ private async Task HandleProcessChanges(Instance instance, List? break; case InstanceEventType.process_AbandonTask: await task.HandleTaskAbandon(elementId, instance); - await _instanceClient.UpdateProcess(instance); break; case InstanceEventType.process_EndEvent: await _appEvents.OnEndAppEvent(instanceEvent.ProcessInfo?.EndEvent, instance); diff --git a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs b/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs index 9a9b9868d..a6619c45f 100644 --- a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs @@ -9,6 +9,7 @@ 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; @@ -133,7 +134,8 @@ public async void OnEndProcessTask_calls_all_added_implementations_of_IProcessTa _layoutStateInitializer); var instance = new Instance() { - Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d" + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", }; await te.OnEndProcessTask("Task_1", instance); _metaMock.Verify(r => r.GetApplicationMetadata()); @@ -195,7 +197,7 @@ public async void OnEndProcessTask_removes_all_shadow_fields_and_saves_to_specif _metaMock.Verify(r => r.GetApplicationMetadata()); _dataMock.Verify(r => r.InsertFormData(It.IsAny(), instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, "model-clean")); _dataMock.Verify(r => r.GetFormData(instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)); - _dataMock.Verify(r => r.Update(instance, instance.Data[0])); + _dataMock.Verify(r => r.LockDataElement(It.Is(i => i.InstanceOwnerPartyId == 1337 && i.InstanceGuid == instanceGuid), new Guid(instance.Data[0].Id))); } [Fact] @@ -250,7 +252,7 @@ public async void OnEndProcessTask_removes_all_shadow_fields_and_saves_to_curren _metaMock.Verify(r => r.GetApplicationMetadata()); _dataMock.Verify(r => r.UpdateData(It.IsAny(), instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)); _dataMock.Verify(r => r.GetFormData(instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)); - _dataMock.Verify(r => r.Update(instance, instance.Data[0])); + _dataMock.Verify(r => r.LockDataElement(It.Is(i => i.InstanceOwnerPartyId == 1337 && i.InstanceGuid == instanceGuid), new Guid(instance.Data[0].Id))); } [Fact] @@ -372,6 +374,7 @@ public async void OnEndProcessTask_does_not_sets_hard_soft_delete_if_process_end var instance = new Instance() { Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", InstanceOwner = new() { PartyId = "1000" @@ -386,6 +389,68 @@ public async void OnEndProcessTask_does_not_sets_hard_soft_delete_if_process_end _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() + { + _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, + _featureManagerMock.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() + { + new() + { + Id = "ba0678ad-960d-4307-aba2-ba29c9804c9d", + 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() { @@ -410,6 +475,7 @@ public async void OnEndProcessTask_sets_hard_soft_delete_if_process_ended_and_au var instance = new Instance() { Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", InstanceOwner = new() { PartyId = "1000" @@ -448,6 +514,7 @@ public async void OnEndProcessTask_does_not_sets_hard_soft_delete_if_process_not var instance = new Instance() { Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", InstanceOwner = new() { PartyId = "1000" @@ -483,6 +550,7 @@ public async void OnEndProcessTask_does_not_sets_hard_soft_delete_if_process_nul var instance = new Instance() { Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/test", InstanceOwner = new() { PartyId = "1000" diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs index 612101176..1728f091c 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs @@ -71,6 +71,39 @@ public async Task InsertBinaryData_MethodProduceValidPlatformRequest() Assert.NotNull(platformRequest); AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Post, "\"a cats story.pdf\"", "application/pdf"); } + + [Fact] + public async Task InsertBinaryData_MethodProduceValidPlatformRequest_with_generatedFrom_query_params() + { + // Arrange + HttpRequestMessage? platformRequest = null; + + var target = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + platformRequest = request; + + DataElement dataElement = new DataElement + { + Id = "DataElement.Id", + InstanceGuid = "InstanceGuid" + }; + await Task.CompletedTask; + return new HttpResponseMessage() { Content = JsonContent.Create(dataElement) }; + }); + + var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); + var instanceIdentifier = new InstanceIdentifier(323413, Guid.NewGuid()); + Uri expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data?dataType=catstories&generatedFromTask=Task_1", UriKind.RelativeOrAbsolute); + + // Act + DataElement actual = await target.InsertBinaryData(instanceIdentifier.ToString(), "catstories", "application/pdf", "a cats story.pdf", stream, "Task_1"); + + // Assert + Assert.NotNull(actual); + + Assert.NotNull(platformRequest); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Post, "\"a cats story.pdf\"", "application/pdf"); + } [Fact] public async Task GetFormData_MethodProduceValidPlatformRequest_ReturnedFormIsValid() @@ -334,7 +367,7 @@ public async Task GetBinaryDataList_returns_AttachemtList_when_DataElements_foun }; response.Should().BeEquivalentTo(expectedList); } - + [Fact] public async Task GetBinaryDataList_throws_PlatformHttpException_if_non_ok_response() { @@ -352,7 +385,7 @@ public async Task GetBinaryDataList_throws_PlatformHttpException_if_non_ok_respo actual.Should().NotBeNull(); actual.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); } - + [Fact] public async Task DeleteBinaryData_returns_true_when_data_was_deleted() { @@ -375,7 +408,7 @@ public async Task DeleteBinaryData_returns_true_when_data_was_deleted() AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); result.Should().BeTrue(); } - + [Fact] public async Task DeleteBinaryData_throws_PlatformHttpException_when_dataelement_not_found() { @@ -397,7 +430,7 @@ public async Task DeleteBinaryData_throws_PlatformHttpException_when_dataelement AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); actual.Response.StatusCode.Should().Be(HttpStatusCode.NotFound); } - + [Fact] public async Task DeleteData_returns_true_when_data_was_deleted_with_delay_true() { @@ -446,7 +479,7 @@ public async Task UpdateData_serializes_and_updates_formdata() platformRequest?.Should().NotBeNull(); AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, null, "application/xml"); } - + [Fact] public async Task UpdateData_throws_error_if_serilization_fails() { @@ -467,7 +500,7 @@ public async Task UpdateData_throws_error_if_serilization_fails() await Assert.ThrowsAsync(async () => await dataClient.UpdateData(exampleModel, instanceIdentifier.InstanceGuid, typeof(DataElement), "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, dataGuid)); invocations.Should().Be(0); } - + [Fact] public async Task UpdateData_throws_platformhttpexception_if_platform_request_fails() { @@ -488,13 +521,110 @@ public async Task UpdateData_throws_platformhttpexception_if_platform_request_fa return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; }); var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", UriKind.RelativeOrAbsolute); - var result = await Assert.ThrowsAsync(async () => await dataClient.UpdateData(exampleModel, instanceIdentifier.InstanceGuid, typeof(ExampleModel), "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, dataGuid)); + var result = await Assert.ThrowsAsync(async () => + await dataClient.UpdateData(exampleModel, instanceIdentifier.InstanceGuid, typeof(ExampleModel), "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, dataGuid)); invocations.Should().Be(1); platformRequest?.Should().NotBeNull(); AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, null, "application/xml"); result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); } + [Fact] + public async Task LockDataElement_calls_lock_endpoint_in_storage_and_returns_updated_DataElement() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + DataElement dataElement = new() + { + Id = "67a5ef12-6e38-41f8-8b42-f91249ebcec0", + Locked = true + }; + var dataClient = GetDataClient(async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, Content = new StringContent("{\"id\":\"67a5ef12-6e38-41f8-8b42-f91249ebcec0\",\"locked\":true}") }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", UriKind.RelativeOrAbsolute); + var response = await dataClient.LockDataElement(instanceIdentifier, dataGuid); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + response.Should().BeEquivalentTo(dataElement); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put); + } + + [Fact] + public async Task LockDataElement_throws_platformhttpexception_if_platform_request_fails() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + HttpRequestMessage? platformRequest = null; + var dataClient = GetDataClient(async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", UriKind.RelativeOrAbsolute); + var result = await Assert.ThrowsAsync(async () => await dataClient.LockDataElement(instanceIdentifier, dataGuid)); + invocations.Should().Be(1); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put); + result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + + [Fact] + public async Task UnlockDataElement_calls_lock_endpoint_in_storage_and_returns_updated_DataElement() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + DataElement dataElement = new() + { + Id = "67a5ef12-6e38-41f8-8b42-f91249ebcec0", + Locked = true + }; + var dataClient = GetDataClient(async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, Content = new StringContent("{\"id\":\"67a5ef12-6e38-41f8-8b42-f91249ebcec0\",\"locked\":true}") }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", UriKind.RelativeOrAbsolute); + var response = await dataClient.UnlockDataElement(instanceIdentifier, dataGuid); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + response.Should().BeEquivalentTo(dataElement); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + } + + [Fact] + public async Task UnlockDataElement_throws_platformhttpexception_if_platform_request_fails() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + HttpRequestMessage? platformRequest = null; + var dataClient = GetDataClient(async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}/lock", UriKind.RelativeOrAbsolute); + var result = await Assert.ThrowsAsync(async () => await dataClient.UnlockDataElement(instanceIdentifier, dataGuid)); + invocations.Should().Be(1); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + private DataClient GetDataClient(Func> handlerFunc) { DelegatingHandlerStub delegatingHandlerStub = new(handlerFunc); @@ -513,6 +643,7 @@ private void AssertHttpRequest(HttpRequestMessage actual, Uri expectedUri, HttpM actual.Content?.Headers.TryGetValues("Content-Disposition", out actualContentDisposition); var authHeader = actual.Headers.Authorization; actual.RequestUri.Should().BeEquivalentTo(expectedUri); + actual.Method.Should().BeEquivalentTo(method); Uri.Compare(actual.RequestUri, expectedUri, UriComponents.HttpRequestUrl, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase).Should().Be(0, "Actual request Uri did not match expected Uri"); if (expectedContentType is not null) { diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index 7d59a359f..c4663464a 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -121,16 +121,16 @@ public async Task GenerateAndStorePdf() }; // Act - await target.GenerateAndStorePdf(instance, CancellationToken.None); + await target.GenerateAndStorePdf(instance, "Task_1", CancellationToken.None); // Asserts _pdfGeneratorClient.Verify( s => s.GeneratePdf( It.Is( u => u.Scheme == "https" && - u.Host == $"{instance.Org}.apps.{HostName}" && - u.AbsoluteUri.Contains(instance.AppId) && - u.AbsoluteUri.Contains(instance.Id)), + u.Host == $"{instance.Org}.apps.{HostName}" && + u.AbsoluteUri.Contains(instance.AppId) && + u.AbsoluteUri.Contains(instance.Id)), It.IsAny()), Times.Once); @@ -140,7 +140,84 @@ public async Task GenerateAndStorePdf() It.Is(s => s == "ref-data-as-pdf"), It.Is(s => s == "application/pdf"), It.Is(s => s == "not-really-an-app.pdf"), - It.IsAny()), + It.IsAny(), + It.Is(s => s == "Task_1")), + Times.Once); + } + + [Fact] + public async Task GenerateAndStorePdf_with_generatedFrom() + { + // Arrange + _pdfGeneratorClient.Setup(s => s.GeneratePdf(It.IsAny(), It.IsAny())); + + _generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}"; + + var target = new PdfService( + _pdf.Object, + _appResources.Object, + _pdfOptionsMapping.Object, + _dataClient.Object, + _httpContextAccessor.Object, + _profile.Object, + _register.Object, + pdfFormatter.Object, + _pdfGeneratorClient.Object, + _pdfGeneratorSettingsOptions, + _generalSettingsOptions); + + var dataModelId = Guid.NewGuid(); + var attachmentId = Guid.NewGuid(); + + Instance instance = new() + { + Id = $"509378/{Guid.NewGuid()}", + AppId = "digdir/not-really-an-app", + Org = "digdir", + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + }, + Data = new() + { + new() + { + Id = dataModelId.ToString(), + DataType = "Model" + }, + new() + { + Id = attachmentId.ToString(), + DataType = "attachment" + } + } + }; + + // Act + await target.GenerateAndStorePdf(instance, "Task_1", CancellationToken.None); + + // Asserts + _pdfGeneratorClient.Verify( + s => s.GeneratePdf( + It.Is( + u => u.Scheme == "https" && + u.Host == $"{instance.Org}.apps.{HostName}" && + u.AbsoluteUri.Contains(instance.AppId) && + u.AbsoluteUri.Contains(instance.Id)), + It.IsAny()), + Times.Once); + + _dataClient.Verify( + s => s.InsertBinaryData( + It.Is(s => s == instance.Id), + It.Is(s => s == "ref-data-as-pdf"), + It.Is(s => s == "application/pdf"), + It.Is(s => s == "not-really-an-app.pdf"), + It.IsAny(), + It.Is(s => s == "Task_1")), Times.Once); } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs index 43b19640f..342e97f17 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEventDispatcherTests.cs @@ -443,7 +443,7 @@ public async Task UpdateProcessAndDispatchEvents_AbandonTask_feedback_instance_u // Assert result.Should().Be(getInstanceResponse); taskEvents.Verify(t => t.OnAbandonProcessTask("Task_2", instance), Times.Once); - instanceService.Verify(i => i.UpdateProcess(instance), Times.Exactly(2)); + 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(); From df5a91da17c0dd4478ef3efb6449e925c35d5da6 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Wed, 14 Jun 2023 13:55:39 +0200 Subject: [PATCH 05/46] always set FlowType to CompleteCirrentMoveToNext --- .../Controllers/ProcessController.cs | 4 +++- .../Internal/Process/ProcessEngine.cs | 1 + .../Internal/Process/ProcessEngineTest.cs | 16 ++++++++++++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 13e75d260..af24bbbe3 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -281,7 +281,8 @@ public async Task> NextElement( { return Forbid(); } - + + _logger.LogDebug("User is authorized to perform action {Action}", checkedAction); var request = new ProcessNextRequest() { Instance = instance, @@ -306,6 +307,7 @@ public async Task> NextElement( } catch (PlatformHttpException e) { + _logger.LogError("Platform exception when processing next. {message}", e.Message); return HandlePlatformHttpException(e, "Process next failed."); } catch (Exception exception) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index b206f5d54..7a17004ef 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -266,6 +266,7 @@ private async Task> MoveProcessToNext( Name = nextElement!.Name, Started = now, AltinnTaskType = task?.ExtensionElements?.TaskExtension?.TaskType, + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), Validated = null, }; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index f06e4d029..144d3cc3e 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -125,6 +125,7 @@ public async Task StartProcess_starts_process_and_moves_to_first_task() ElementId = "Task_1", Flow = 2, AltinnTaskType = "data", + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), Name = "Utfylling" }, StartEvent = "StartEvent_1" @@ -187,7 +188,7 @@ public async Task StartProcess_starts_process_and_moves_to_first_task() _processEventDispatcherMock.Verify(d => d.UpdateProcessAndDispatchEvents( It.Is(i => CompareInstance(expectedInstance, i)), null, - It.Is>(l => CompareInstanceEvents(l, expectedInstanceEvents)))); + It.Is>(l => CompareInstanceEvents(expectedInstanceEvents, l)))); result.Success.Should().BeTrue(); } @@ -229,6 +230,7 @@ public async Task StartProcess_starts_process_and_moves_to_first_task_with_prefi ElementId = "Task_1", Flow = 2, AltinnTaskType = "data", + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), Name = "Utfylling" }, StartEvent = "StartEvent_1" @@ -335,6 +337,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() ElementId = "Task_2", Flow = 3, AltinnTaskType = "confirmation", + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), Name = "Bekreft" }, StartEvent = "StartEvent_1" @@ -422,6 +425,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() ElementId = "Task_2", Name = "Bekreft", AltinnTaskType = "confirmation", + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), Flow = 3 } } @@ -458,6 +462,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_abandon_instance ElementId = "Task_2", Flow = 3, AltinnTaskType = "confirmation", + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), Name = "Bekreft" }, StartEvent = "StartEvent_1" @@ -545,6 +550,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_abandon_instance ElementId = "Task_2", Name = "Bekreft", AltinnTaskType = "confirmation", + FlowType = ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), Flow = 3 } } @@ -923,6 +929,12 @@ public static bool JsonCompare(object expected, object actual) var expectedJson = JsonConvert.SerializeObject(expected); var actualJson = JsonConvert.SerializeObject(actual); - return expectedJson == actualJson; + var jsonCompare = expectedJson == actualJson; + if (jsonCompare == false) + { + Console.WriteLine("Not equal"); + } + + return jsonCompare; } } From fc9f0aa8334384cef75d4f57b2d87550e11132a2 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Fri, 16 Jun 2023 10:23:39 +0200 Subject: [PATCH 06/46] Remove old TaskType field and make action element value instead of attribute (#260) --- .../Controllers/ProcessController.cs | 2 +- .../AltinnExtensionProperties/AltinnAction.cs | 4 ++-- .../Internal/Process/Elements/ProcessTask.cs | 6 ------ .../Internal/Process/ProcessNavigatorTests.cs | 20 ++++++++++++------- .../Internal/Process/ProcessReaderTests.cs | 3 +-- .../TestData/simple-gateway-default.bpmn | 6 +++--- .../simple-gateway-with-join-gateway.bpmn | 4 ++-- .../Process/TestData/simple-gateway.bpmn | 12 +++++------ .../Process/TestData/simple-linear-both.bpmn | 6 +++--- .../Process/TestData/simple-linear-new.bpmn | 6 +++--- .../Process/TestData/simple-linear.bpmn | 16 ++++++++++++--- 11 files changed, 47 insertions(+), 38 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index af24bbbe3..f6bc3e361 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -461,7 +461,7 @@ private async Task ConvertAndAuthorizeActions(string org, strin appProcessState.CurrentTask.Actions = new Dictionary(); foreach (AltinnAction action in processTask.ExtensionElements?.TaskExtension?.AltinnActions ?? new List()) { - appProcessState.CurrentTask.Actions.Add(action.Id, await AuthorizeAction(action.Id, org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id)); + appProcessState.CurrentTask.Actions.Add(action.Value, await AuthorizeAction(action.Value, org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id)); } appProcessState.CurrentTask.HasWriteAccess = await AuthorizeAction("write", org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id); diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs index 7450dd04b..23fa62873 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs @@ -10,7 +10,7 @@ public class AltinnAction /// /// Gets or sets the ID of the action /// - [XmlAttribute("id", Namespace = "http://altinn.no/process")] - public string Id { get; set; } + [XmlText] + public string Value { get; set; } } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs b/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs index 70b84c207..b80957b8e 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/ProcessTask.cs @@ -8,12 +8,6 @@ namespace Altinn.App.Core.Internal.Process.Elements /// public class ProcessTask: ProcessElement { - /// - /// Gets or sets the outgoing id of a task - /// - [XmlAttribute("tasktype", Namespace = "http://altinn.no")] - public string? TaskType { get; set; } - /// /// Defines the extension elements /// diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs index f90fe8116..4d06f43f1 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs @@ -1,6 +1,7 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Internal.Process.Elements.Base; using Altinn.App.Core.Tests.Internal.Process.TestUtils; using Altinn.App.PlatformServices.Tests.Internal.Process.StubGatewayFilters; @@ -22,9 +23,16 @@ public async void GetNextTask_returns_next_element_if_no_gateway() { Id = "Task2", Name = "Bekreft skjemadata", - TaskType = "confirmation", Incoming = new List { "Flow2" }, - Outgoing = new List { "Flow3" } + Outgoing = new List { "Flow3" }, + ExtensionElements = new ExtensionElements() + { + TaskExtension = new() + { + TaskType = "confirmation", + AltinnActions = new() + }, + } }); } @@ -45,7 +53,6 @@ public async void GetNextTask_returns_default_if_no_filtering_is_implemented_and { Id = "Task2", Name = null!, - TaskType = null!, ExtensionElements = new() { TaskExtension = new() @@ -55,11 +62,11 @@ public async void GetNextTask_returns_default_if_no_filtering_is_implemented_and { new() { - Id = "confirm" + Value = "confirm" }, new() { - Id = "reject" + Value = "reject" } } } @@ -89,7 +96,6 @@ public async void GetNextTask_runs_custom_filter_and_returns_result() { Id = "Task2", Name = null!, - TaskType = null!, ExtensionElements = new() { TaskExtension = new() @@ -99,7 +105,7 @@ public async void GetNextTask_runs_custom_filter_and_returns_result() { new() { - Id = "submit" + Value = "submit" } } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs index 17ed16b72..530784d12 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs @@ -101,7 +101,6 @@ public void GetNextElement_returns_task() Incoming = new List { "Flow2" }, Outgoing = new List { "Flow3" }, Name = "Bekreft skjemadata", - TaskType = "confirmation" } }); } @@ -316,7 +315,7 @@ public void GetFlowElement_returns_ProcessTask_with_id() { new() { - Id = "submit", + Value = "submit", } }, TaskType = "data", diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn index bb23facd4..677c020af 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn @@ -17,7 +17,7 @@ - + submit data @@ -42,8 +42,8 @@ - - + confirm + reject confirm diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn index d0b809469..054dde9a2 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-with-join-gateway.bpmn @@ -17,7 +17,7 @@ - + submit data @@ -37,7 +37,7 @@ - + submit data diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway.bpmn index d5839d503..ced273995 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway.bpmn @@ -3,7 +3,7 @@ xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" - xmlns:altinn="http://altinn.no" + xmlns:altinn="http://altinn.no/process" id="Definitions_1eqx4ru" targetNamespace="http://bpmn.io/schema/bpmn" exporter="bpmn-js (https://demo.bpmn.io)" exporterVersion="10.2.0"> @@ -17,9 +17,9 @@ - + submit - + data @@ -37,10 +37,10 @@ - - + confirm + reject - + confirm diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-both.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-both.bpmn index 06dc5c065..f23ed4872 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-both.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-both.bpmn @@ -18,7 +18,7 @@ xmlns:altinn="http://altinn.no"> - + submit data2 @@ -31,8 +31,8 @@ xmlns:altinn="http://altinn.no"> - - + confirm + reject confirmation2 diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-new.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-new.bpmn index 3422a65c8..599f7d91a 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-new.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear-new.bpmn @@ -19,7 +19,7 @@ xmlns:altinn="http://altinn.no"> data - + submit @@ -31,8 +31,8 @@ xmlns:altinn="http://altinn.no"> - - + confirm + reject confirmation diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear.bpmn index f2db44b72..f28a30879 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-linear.bpmn @@ -6,20 +6,30 @@ xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" targetNamespace="http://bpmn.io/schema/bpmn" -xmlns:altinn="http://altinn.no"> +xmlns:altinn="http://altinn.no/process"> Flow1 - + Flow1 Flow2 + + + data + + - + Flow2 Flow3 + + + confirmation + + From 7e7b8b8be0cda563a3663142ba5902a4ac0a79e3 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Fri, 23 Jun 2023 11:32:20 +0200 Subject: [PATCH 07/46] Feature/262 custom action authorization (#264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add logic to write custom authorization logic for actions in a process task * Add trailing linebreaks * trailing linebreaks * fix check. If taskId from input is null UserActionAuthorizer.TaskId also needs to be null. * Enable nullability in AuthorizationServiceTests.cs * remove nullability in AuthorizationServiceTests.cs as is not needed * Add tests for GetPartyList and ValidateSelectedParty * Refactor taskextension to group signature configuration * Fix typoœœ * check for missing config * Add missing xml comment * add test for missing config for SigningUserAction * verify no more calls to sign client * fix some codesmells --- .../Controllers/ProcessController.cs | 5 +- .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Features/Action/SigningUserAction.cs | 11 +- .../Action/UserActionAuthorizerContext.cs | 45 +++ .../Features/IUserActionAuthorizer.cs | 16 ++ .../Internal/Auth/AuthorizationService.cs | 67 +++++ .../Internal/Auth/IAuthorizationService.cs | 38 +++ .../Action/IUserActionAuthorizerProvider.cs | 24 ++ .../Action/UserActionAuthorizerProvider.cs | 32 +++ ...ionAuthorizerServiceCollectionExtension.cs | 66 +++++ .../AltinnSignatureConfiguration.cs | 29 ++ .../AltinnTaskExtension.cs | 16 +- .../Features/Action/SigningUserActionTests.cs | 47 ++- .../signing-task-process-missing-config.bpmn | 50 ++++ .../Action/TestData/signing-task-process.bpmn | 10 +- .../Auth/AuthorizationServiceTests.cs | 271 ++++++++++++++++++ .../TestData/UserActionAuthorizerStub.cs | 13 + ...thorizerServiceCollectionExtensionTests.cs | 127 ++++++++ .../Internal/Process/ProcessReaderTests.cs | 17 +- .../TestData/simple-gateway-default.bpmn | 15 +- 20 files changed, 866 insertions(+), 34 deletions(-) create mode 100644 src/Altinn.App.Core/Features/Action/UserActionAuthorizerContext.cs create mode 100644 src/Altinn.App.Core/Features/IUserActionAuthorizer.cs create mode 100644 src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs create mode 100644 src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Action/IUserActionAuthorizerProvider.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerProvider.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtension.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnSignatureConfiguration.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process-missing-config.bpmn create mode 100644 test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/Action/TestData/UserActionAuthorizerStub.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtensionTests.cs diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index f6bc3e361..90904fcd0 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using AppProcessState = Altinn.App.Core.Internal.Process.Elements.AppProcessState; +using IAuthorizationService = Altinn.App.Core.Internal.Auth.IAuthorizationService; namespace Altinn.App.Api.Controllers { @@ -34,7 +35,7 @@ public class ProcessController : ControllerBase private readonly IInstanceClient _instanceClient; private readonly IProcessClient _processClient; private readonly IValidation _validationService; - private readonly IAuthorizationClient _authorization; + private readonly IAuthorizationService _authorization; private readonly IProcessEngine _processEngine; private readonly IProcessReader _processReader; @@ -46,7 +47,7 @@ public ProcessController( IInstanceClient instanceClient, IProcessClient processClient, IValidation validationService, - IAuthorizationClient authorization, + IAuthorizationService authorization, IProcessReader processReader, IProcessEngine processEngine) { diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 8deff2af6..e4480613f 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -90,6 +90,7 @@ public static void AddPlatformServices(this IServiceCollection services, IConfig services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); + services.TryAddTransient(); } private static void AddApplicationIdentifier(IServiceCollection services) diff --git a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs index 8d7196d36..db065791e 100644 --- a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs +++ b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs @@ -43,19 +43,22 @@ public SigningUserAction(IProcessReader processReader, ILogger /// + /// public async Task HandleAction(UserActionContext context) { if (_processReader.GetFlowElement(context.Instance.Process.CurrentTask.ElementId) is ProcessTask currentTask) { _logger.LogInformation("Signing action handler invoked for instance {Id}. In task: {CurrentTaskId}", context.Instance.Id, currentTask.Id); - var dataTypes = currentTask.ExtensionElements?.TaskExtension?.DataTypesToSign ?? new(); + var dataTypes = currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.DataTypesToSign ?? new(); var connectedDataElements = GetDataElementSignatures(context.Instance.Data, dataTypes); - if (connectedDataElements.Count > 0) + if (connectedDataElements.Count > 0 && currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType != null) { - SignatureContext signatureContext = new SignatureContext(new InstanceIdentifier(context.Instance), currentTask.ExtensionElements?.TaskExtension?.SignatureDataType, await GetSignee(context.UserId), connectedDataElements); + SignatureContext signatureContext = new SignatureContext(new InstanceIdentifier(context.Instance), currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType!, await GetSignee(context.UserId), connectedDataElements); await _signClient.SignDataElements(signatureContext); + return true; } - return true; + + throw new ApplicationConfigException("Missing configuration for signing. Check that the task has a signature configuration and that the data types to sign are defined."); } return false; diff --git a/src/Altinn.App.Core/Features/Action/UserActionAuthorizerContext.cs b/src/Altinn.App.Core/Features/Action/UserActionAuthorizerContext.cs new file mode 100644 index 000000000..145240843 --- /dev/null +++ b/src/Altinn.App.Core/Features/Action/UserActionAuthorizerContext.cs @@ -0,0 +1,45 @@ +using System.Security.Claims; +using Altinn.App.Core.Models; + +namespace Altinn.App.Core.Features.Action; + +/// +/// Context for authorization of user actions +/// +public class UserActionAuthorizerContext +{ + /// + /// Initializes a new instance of the class + /// + /// for the user + /// for the instance + /// The id of the task + /// The action to authorize + public UserActionAuthorizerContext(ClaimsPrincipal user, InstanceIdentifier instanceIdentifier, string? taskId, string action) + { + User = user; + InstanceIdentifier = instanceIdentifier; + TaskId = taskId; + Action = action; + } + + /// + /// Gets or sets the user + /// + public ClaimsPrincipal User { get; set; } + + /// + /// Gets or sets the instance identifier + /// + public InstanceIdentifier InstanceIdentifier { get; set; } + + /// + /// Gets or sets the task id + /// + public string? TaskId { get; set; } + + /// + /// Gets or sets the action + /// + public string Action { get; set; } +} diff --git a/src/Altinn.App.Core/Features/IUserActionAuthorizer.cs b/src/Altinn.App.Core/Features/IUserActionAuthorizer.cs new file mode 100644 index 000000000..ec1c6ab5e --- /dev/null +++ b/src/Altinn.App.Core/Features/IUserActionAuthorizer.cs @@ -0,0 +1,16 @@ +using Altinn.App.Core.Features.Action; + +namespace Altinn.App.Core.Features; + +/// +/// Interface for writing custom authorization logic for actions in the app that cannot be handled by the default authorization policies +/// +public interface IUserActionAuthorizer +{ + /// + /// Authorizes the action in the given context + /// + /// for the action to authorize + /// true if user is authorized to perform the action, false if not + Task AuthorizeAction(UserActionAuthorizerContext context); +} diff --git a/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs b/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs new file mode 100644 index 000000000..50c49cda0 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs @@ -0,0 +1,67 @@ +using System.Security.Claims; +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Internal.Process.Action; +using Altinn.App.Core.Models; +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Core.Internal.Auth; + +/// +/// Service that handles authorization. Uses AuthorizationClient to communicate with authorization component. Makes authorization decisions in app context possible +/// +public class AuthorizationService : IAuthorizationService +{ + private readonly IAuthorizationClient _authorizationClient; + private readonly IEnumerable _userActionAuthorizers; + + /// + /// Initializes a new instance of the class + /// + /// The authorization client + /// The user action authorizers + public AuthorizationService(IAuthorizationClient authorizationClient, IEnumerable userActionAuthorizers) + { + _authorizationClient = authorizationClient; + _userActionAuthorizers = userActionAuthorizers; + } + + /// + public async Task?> GetPartyList(int userId) + { + return await _authorizationClient.GetPartyList(userId); + } + + /// + public async Task ValidateSelectedParty(int userId, int partyId) + { + return await _authorizationClient.ValidateSelectedParty(userId, partyId); + } + + /// + public async Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null) + { + if (!await _authorizationClient.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)) + { + return false; + } + + foreach (var authorizerRegistrator in _userActionAuthorizers.Where(a => IsAuthorizerForTaskAndAction(a, taskId, action))) + { + var context = new UserActionAuthorizerContext(user, instanceIdentifier, taskId, action); + if (!await authorizerRegistrator.Authorizer.AuthorizeAction(context)) + { + return false; + } + } + + return true; + } + + private static bool IsAuthorizerForTaskAndAction(IUserActionAuthorizerProvider authorizer, string? taskId, string action) + { + return (authorizer.TaskId == null && authorizer.Action == null) + || (authorizer.TaskId == null && authorizer.Action == action) + || (authorizer.TaskId == taskId && authorizer.Action == null) + || (authorizer.TaskId == taskId && authorizer.Action == action); + } +} diff --git a/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs b/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs new file mode 100644 index 000000000..79c89c2be --- /dev/null +++ b/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs @@ -0,0 +1,38 @@ +using System.Security.Claims; +using Altinn.App.Core.Models; +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Core.Internal.Auth +{ + /// + /// Interface for authorization functionality. + /// + public interface IAuthorizationService + { + /// + /// Returns the list of parties that user has any rights for. + /// + /// The userId. + /// List of parties. + Task?> GetPartyList(int userId); + + /// + /// Verifies that the selected party is contained in the user's party list. + /// + /// The user id. + /// The party id. + /// Boolean indicating whether or not the user can represent the selected party. + Task ValidateSelectedParty(int userId, int partyId); + + /// + /// Check if the user is authorized to perform the given action on the given instance. + /// + /// + /// + /// + /// + /// + /// + Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null); + } +} diff --git a/src/Altinn.App.Core/Internal/Process/Action/IUserActionAuthorizerProvider.cs b/src/Altinn.App.Core/Internal/Process/Action/IUserActionAuthorizerProvider.cs new file mode 100644 index 000000000..49efd787e --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Action/IUserActionAuthorizerProvider.cs @@ -0,0 +1,24 @@ +using Altinn.App.Core.Features; + +namespace Altinn.App.Core.Internal.Process.Action; + +/// +/// Register a user action authorizer for a given action and/or task +/// +public interface IUserActionAuthorizerProvider +{ + /// + /// Gets or sets the action + /// + public string? Action { get; } + + /// + /// Gets or sets the task id + /// + public string? TaskId { get; } + + /// + /// Gets or sets the authorizer implementation + /// + public IUserActionAuthorizer Authorizer { get; } +} diff --git a/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerProvider.cs b/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerProvider.cs new file mode 100644 index 000000000..39b55dbf2 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerProvider.cs @@ -0,0 +1,32 @@ +using Altinn.App.Core.Features; + +namespace Altinn.App.Core.Internal.Process.Action; + +/// +/// Register a user action authorizer for a given action and/or task +/// +public class UserActionAuthorizerProvider: IUserActionAuthorizerProvider +{ + + /// + /// Initializes a new instance of the class + /// + /// + /// + /// + public UserActionAuthorizerProvider(string? taskId, string? action, IUserActionAuthorizer authorizer) + { + TaskId = taskId; + Action = action; + Authorizer = authorizer; + } + + /// + public string? Action { get; set; } + + /// + public string? TaskId { get; set; } + + /// + public IUserActionAuthorizer Authorizer { get; set; } +} diff --git a/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtension.cs b/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtension.cs new file mode 100644 index 000000000..dbb2a0bcf --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtension.cs @@ -0,0 +1,66 @@ +using Altinn.App.Core.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Altinn.App.Core.Internal.Process.Action; + +/// +/// Extension methods for adding user action authorizers to the service collection connected to a action and/or task +/// +public static class UserActionAuthorizerServiceCollectionExtension +{ + /// + /// Adds a transient user action authorizer to the service collection connected to a action and task + /// + /// ServiceCollection + /// Id of the task the authorizer should run for + /// Name of the action the authorizer should run for + /// Implementation if that should be executed + /// + public static IServiceCollection AddTransientUserActionAuthorizerForActionInTask(this IServiceCollection services, string taskId, string action) where T : class, IUserActionAuthorizer + { + return services.RegisterUserActionAuthorizer(taskId, action); + } + + /// + /// Adds a transient user action authorizer to the service collection connected to a action in all tasks + /// + /// ServiceCollection + /// Name of the action the authorizer should run for + /// Implementation if that should be executed + /// + public static IServiceCollection AddTransientUserActionAuthorizerForActionInAllTasks(this IServiceCollection services, string action) where T : class, IUserActionAuthorizer + { + return services.RegisterUserActionAuthorizer(null, action); + } + + /// + /// Adds a transient user action authorizer to the service collection connected to all actions in a task + /// + /// ServiceCollection + /// Name of the action the authorizer should run for + /// Implementation if that should be executed + /// + public static IServiceCollection AddTransientUserActionAuthorizerForAllActionsInTask(this IServiceCollection services, string taskId) where T : class, IUserActionAuthorizer + { + return services.RegisterUserActionAuthorizer(taskId, null); + } + + /// + /// Adds a transient user action authorizer to the service collection connected to all actions in all tasks + /// + /// ServiceCollection + /// Implementation if that should be executed + /// + public static IServiceCollection AddTransientUserActionAuthorizerForAllActionsInAllTasks(this IServiceCollection services) where T : class, IUserActionAuthorizer + { + return services.RegisterUserActionAuthorizer(null, null); + } + + private static IServiceCollection RegisterUserActionAuthorizer(this IServiceCollection services, string? taskId, string? action) where T : class, IUserActionAuthorizer + { + services.TryAddTransient(); + services.AddTransient(sp => new UserActionAuthorizerProvider(taskId, action, sp.GetRequiredService())); + return services; + } +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnSignatureConfiguration.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnSignatureConfiguration.cs new file mode 100644 index 000000000..c980c53fa --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnSignatureConfiguration.cs @@ -0,0 +1,29 @@ +using System.Xml.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +/// +/// Configuration properties for signatures in a process task +/// +public class AltinnSignatureConfiguration +{ + /// + /// Define what taskId that should be signed for signing tasks + /// + [XmlArray(ElementName = "dataTypesToSign", Namespace = "http://altinn.no/process", IsNullable = true)] + [XmlArrayItem(ElementName = "dataType", Namespace = "http://altinn.no/process")] + public List DataTypesToSign { get; set; } = new(); + + /// + /// Set what dataTypeId that should be used for storing the signature + /// + [XmlElement("signatureDataType", Namespace = "http://altinn.no/process")] + public string SignatureDataType { get; set; } + + /// + /// Define what signature dataypes this signature should be unique from. Users that have sign any of the signatures in the list will not be able to sign this signature + /// + [XmlArray(ElementName = "uniqueFromSignaturesInDataTypes", Namespace = "http://altinn.no/process", IsNullable = true)] + [XmlArrayItem(ElementName = "dataType", Namespace = "http://altinn.no/process")] + public List UniqueFromSignaturesInDataTypes { get; set; } = new(); +} diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs index d45bf9f73..98ff963ee 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnTaskExtension.cs @@ -20,18 +20,12 @@ public class AltinnTaskExtension //[XmlElement(ElementName = "taskType", Namespace = "http://altinn.no/process/task", IsNullable = true)] [XmlElement("taskType", Namespace = "http://altinn.no/process")] public string? TaskType { get; set; } - - /// - /// Define what taskId that should be signed for signing tasks - /// - [XmlArray(ElementName = "dataTypesToSign", Namespace = "http://altinn.no/process", IsNullable = true)] - [XmlArrayItem(ElementName = "dataType", Namespace = "http://altinn.no/process")] - public List DataTypesToSign { get; set; } = new(); + /// - /// Set what dataTypeId that should be used for storing the signature + /// Gets or sets the configuration for signature /// - [XmlElement("signatureDataType", Namespace = "http://altinn.no/process")] - public string SignatureDataType { get; set; } + [XmlElement("signatureConfig", Namespace = "http://altinn.no/process")] + public AltinnSignatureConfiguration? SignatureConfiguration { get; set; } = new AltinnSignatureConfiguration(); } -} \ No newline at end of file +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs index d2bdbe46b..298817399 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs @@ -32,7 +32,7 @@ public async void HandleAction_returns_ok_if_user_is_valid() UserId = 1337, Party = new Party() { SSN = "12345678901" } }; - (var userAction, var signClientMock)= CreateSigningUserAction(userProfile); + (var userAction, var signClientMock) = CreateSigningUserAction(userProfile); var instance = new Instance() { Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", @@ -65,11 +65,52 @@ public async void HandleAction_returns_ok_if_user_is_valid() SignatureContext expected = new SignatureContext(new InstanceIdentifier(instance), "signature", new Signee() { UserId = "1337", PersonNumber = "12345678901" }, new DataElementSignature("a499c3ef-e88a-436b-8650-1c43e5037ada")); signClientMock.Verify(s => s.SignDataElements(It.Is(sc => AssertSigningContextAsExpected(sc, expected))), Times.Once); result.Should().BeTrue(); + signClientMock.VerifyNoOtherCalls(); } - private (SigningUserAction SigningUserAction, Mock SignClientMock) CreateSigningUserAction(UserProfile userProfileToReturn = null, PlatformHttpException platformHttpExceptionToThrow = null) + [Fact] + public async void HandleAction_throws_ApplicationConfigException_if_SignatureDataType_is_null() + { + // Arrange + UserProfile userProfile = new UserProfile() + { + UserId = 1337, + Party = new Party() { SSN = "12345678901" } + }; + (var userAction, var signClientMock) = CreateSigningUserAction(userProfileToReturn: userProfile, testBpmnfilename: "signing-task-process-missing-config.bpmn"); + var instance = new Instance() + { + Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", + InstanceOwner = new() + { + PartyId = "5000", + }, + Process = new() + { + CurrentTask = new() + { + ElementId = "Task2" + } + }, + Data = new() + { + new() + { + Id = "a499c3ef-e88a-436b-8650-1c43e5037ada", + DataType = "Model" + } + } + }; + var userActionContext = new UserActionContext(instance, 1337); + + // Act + await Assert.ThrowsAsync(async () => await userAction.HandleAction(userActionContext)); + signClientMock.VerifyNoOtherCalls(); + } + + private static (SigningUserAction SigningUserAction, Mock SignClientMock) CreateSigningUserAction(UserProfile userProfileToReturn = null, PlatformHttpException platformHttpExceptionToThrow = null, string testBpmnfilename = "signing-task-process.bpmn") { - IProcessReader processReader = ProcessTestUtils.SetupProcessReader("signing-task-process.bpmn", Path.Combine("Features", "Action", "TestData")); + IProcessReader processReader = ProcessTestUtils.SetupProcessReader(testBpmnfilename, Path.Combine("Features", "Action", "TestData")); AppSettings appSettings = new AppSettings() { AppBasePath = Path.Combine("Features", "Action"), diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process-missing-config.bpmn b/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process-missing-config.bpmn new file mode 100644 index 000000000..6daea2280 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process-missing-config.bpmn @@ -0,0 +1,50 @@ + + + + + Flow1 + + + + Flow1 + Flow2 + + + + + + data + + + + + + Flow2 + Flow3 + + + + + + + signing + + + Model + + + + + + + + Flow3 + + + diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process.bpmn b/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process.bpmn index 024c830ee..43afdc77c 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process.bpmn +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signing-task-process.bpmn @@ -34,10 +34,12 @@ signing - - Model - - signature + + + Model + + signature + diff --git a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs new file mode 100644 index 000000000..c0349841a --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs @@ -0,0 +1,271 @@ +#nullable enable +using System.Security.Claims; +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.Models; +using Altinn.Platform.Register.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Auth; + +public class AuthorizationServiceTests +{ + [Fact] + public async Task GetPartyList_returns_party_list_from_AuthorizationClient() + { + // Input + int userId = 1337; + + // Arrange + Mock authorizationClientMock = new Mock(); + List partyList = new List(); + authorizationClientMock.Setup(a => a.GetPartyList(userId)).ReturnsAsync(partyList); + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); + + // Act + List? result = await authorizationService.GetPartyList(userId); + + // Assert + result.Should().BeSameAs(partyList); + authorizationClientMock.Verify(a => a.GetPartyList(userId), Times.Once); + } + + [Fact] + public async Task ValidateSelectedParty_returns_validation_from_AuthorizationClient() + { + // Input + int userId = 1337; + int partyId = 1338; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.ValidateSelectedParty(userId, partyId)).ReturnsAsync(true); + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); + + // Act + bool? result = await authorizationService.ValidateSelectedParty(userId, partyId); + + // Assert + result.Should().BeTrue(); + authorizationClientMock.Verify(a => a.ValidateSelectedParty(userId, partyId), Times.Once); + } + + [Fact] + public async Task AuthorizeAction_returns_true_when_AutorizationClient_true_and_no_IUserActinAuthorizerProvider_is_provided() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); + + // Act + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeTrue(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + } + + [Fact] + public async Task AuthorizeAction_returns_false_when_AutorizationClient_false_and_no_IUserActinAuthorizerProvider_is_provided() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(false); + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); + + // Act + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeFalse(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + } + + [Fact] + public async Task AuthorizeAction_returns_false_when_AutorizationClient_true_and_one_IUserActinAuthorizerProvider_returns_false() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); + + Mock userActionAuthorizerMock = new Mock(); + userActionAuthorizerMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(false); + IUserActionAuthorizerProvider userActionAuthorizerProvider = new UserActionAuthorizerProvider("taskId", "action", userActionAuthorizerMock.Object); + + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerProvider }); + + // Act + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeFalse(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + userActionAuthorizerMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); + } + + [Fact] + public async Task AuthorizeAction_does_not_call_UserActionAuthorizer_if_AuthorizationClient_returns_false() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(false); + + Mock userActionAuthorizerMock = new Mock(); + userActionAuthorizerMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); + IUserActionAuthorizerProvider userActionAuthorizerProvider = new UserActionAuthorizerProvider("taskId", "action", userActionAuthorizerMock.Object); + + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerProvider }); + + // Act + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeFalse(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + userActionAuthorizerMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Never); + } + + [Fact] + public async Task AuthorizeAction_calls_all_providers_and_return_true_if_all_true() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); + + Mock userActionAuthorizerOneMock = new Mock(); + userActionAuthorizerOneMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); + IUserActionAuthorizerProvider userActionAuthorizerOneProvider = new UserActionAuthorizerProvider("taskId", "action", userActionAuthorizerOneMock.Object); + Mock userActionAuthorizerTwoMock = new Mock(); + userActionAuthorizerTwoMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); + IUserActionAuthorizerProvider userActionAuthorizerTwoProvider = new UserActionAuthorizerProvider("taskId", "action", userActionAuthorizerTwoMock.Object); + + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerOneProvider, userActionAuthorizerTwoProvider }); + + // Act + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeTrue(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + userActionAuthorizerOneMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); + userActionAuthorizerTwoMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); + } + + [Fact] + public async Task AuthorizeAction_does_not_call_providers_with_non_matching_taskId_and_or_action() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); + + Mock userActionAuthorizerOneMock = new Mock(); + userActionAuthorizerOneMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(false); + IUserActionAuthorizerProvider userActionAuthorizerOneProvider = new UserActionAuthorizerProvider("taskId", "action2", userActionAuthorizerOneMock.Object); + + Mock userActionAuthorizerTwoMock = new Mock(); + userActionAuthorizerTwoMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(false); + IUserActionAuthorizerProvider userActionAuthorizerTwoProvider = new UserActionAuthorizerProvider("taskId2", "action", userActionAuthorizerTwoMock.Object); + + Mock userActionAuthorizerThreeMock = new Mock(); + userActionAuthorizerThreeMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(false); + IUserActionAuthorizerProvider userActionAuthorizerThreeProvider = new UserActionAuthorizerProvider("taskId3", "action3", userActionAuthorizerThreeMock.Object); + + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerOneProvider, userActionAuthorizerTwoProvider, userActionAuthorizerThreeProvider }); + + // Act + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeTrue(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + userActionAuthorizerOneMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Never); + userActionAuthorizerTwoMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Never); + userActionAuthorizerThreeMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Never); + } + + [Fact] + public async Task AuthorizeAction_calls_providers_with_task_null_and_or_action_null() + { + // Input + AppIdentifier appIdentifier = new AppIdentifier("ttd/xunit-app"); + InstanceIdentifier instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId: 1337, instanceGuid: Guid.NewGuid()); + ClaimsPrincipal user = new ClaimsPrincipal(); + string action = "action"; + string taskId = "taskId"; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); + + Mock userActionAuthorizerOneMock = new Mock(); + userActionAuthorizerOneMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); + IUserActionAuthorizerProvider userActionAuthorizerOneProvider = new UserActionAuthorizerProvider(null, "action", userActionAuthorizerOneMock.Object); + + Mock userActionAuthorizerTwoMock = new Mock(); + userActionAuthorizerTwoMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); + IUserActionAuthorizerProvider userActionAuthorizerTwoProvider = new UserActionAuthorizerProvider("taskId", null, userActionAuthorizerTwoMock.Object); + + Mock userActionAuthorizerThreeMock = new Mock(); + userActionAuthorizerThreeMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); + IUserActionAuthorizerProvider userActionAuthorizerThreeProvider = new UserActionAuthorizerProvider(null, null, userActionAuthorizerThreeMock.Object); + + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerOneProvider, userActionAuthorizerTwoProvider, userActionAuthorizerThreeProvider }); + + // Actπ + bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); + + // Assert + result.Should().BeTrue(); + authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); + userActionAuthorizerOneMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); + userActionAuthorizerTwoMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); + userActionAuthorizerThreeMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Action/TestData/UserActionAuthorizerStub.cs b/test/Altinn.App.Core.Tests/Internal/Process/Action/TestData/UserActionAuthorizerStub.cs new file mode 100644 index 000000000..4d3fecb7f --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/Action/TestData/UserActionAuthorizerStub.cs @@ -0,0 +1,13 @@ +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Action; + +namespace Altinn.App.Core.Tests.Internal.Process.Action.TestData +{ + public class UserActionAuthorizerStub: IUserActionAuthorizer + { + public Task AuthorizeAction(UserActionAuthorizerContext context) + { + return Task.FromResult(true); + } + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtensionTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtensionTests.cs new file mode 100644 index 000000000..222235cdd --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/Action/UserActionAuthorizerServiceCollectionExtensionTests.cs @@ -0,0 +1,127 @@ +using Altinn.App.Core.Extensions; +using Altinn.App.Core.Internal.Process.Action; +using Altinn.App.Core.Tests.Internal.Process.Action.TestData; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process.Action; + +public class UserActionAuthorizerServiceCollectionExtensionTests +{ + [Fact] + public void AddTransientUserActionAuthorizerForActionInTask_adds_IUserActinAuthorizerProvider_with_task_and_action_set() + { + // Arrange + string taskId = "Task_1"; + string action = "Action_1"; + IServiceCollection services = new ServiceCollection(); + + // Act + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeFalse(); + services.AddTransientUserActionAuthorizerForActionInTask(taskId, action); + + // Assert + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeTrue(); + services.IsAdded(typeof(UserActionAuthorizerStub)).Should().BeTrue(); + var sp = services.BuildServiceProvider(); + var provider = sp.GetService(); + provider.Should().NotBeNull(); + provider.TaskId.Should().Be(taskId); + provider.Action.Should().Be(action); + provider.Authorizer.Should().BeOfType(); + } + + [Fact] + public void AddTransientUserActionAuthorizerForActionInTask_adds_only_one_UserActionAuthorizerStub_if_used_multiple_times() + { + // Arrange + string taskId = "Task_1"; + string action = "Action_1"; + string taskId2 = "Task_2"; + IServiceCollection services = new ServiceCollection(); + + // Act + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeFalse(); + services.AddTransientUserActionAuthorizerForActionInTask(taskId, action); + services.AddTransientUserActionAuthorizerForActionInTask(taskId2, action); + + // Assert + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeTrue(); + services.IsAdded(typeof(UserActionAuthorizerStub)).Should().BeTrue(); + var sp = services.BuildServiceProvider(); + var authorizer = sp.GetServices(); + authorizer.Should().NotBeNull(); + authorizer.Should().HaveCount(1); + var provider = sp.GetServices(); + provider.Should().NotBeNull(); + provider.Should().HaveCount(2); + provider.Should().ContainEquivalentOf(new UserActionAuthorizerProvider(taskId, action, authorizer.First())); + provider.Should().ContainEquivalentOf(new UserActionAuthorizerProvider(taskId2, action, authorizer.First())); + } + + [Fact] + public void AddTransientUserActionAuthorizerForActionInAllTasks_adds_IUserActinAuthorizerProvider_with_action_set() + { + // Arrange + string action = "Action_1"; + IServiceCollection services = new ServiceCollection(); + + // Act + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeFalse(); + services.AddTransientUserActionAuthorizerForActionInAllTasks(action); + + // Assert + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeTrue(); + services.IsAdded(typeof(UserActionAuthorizerStub)).Should().BeTrue(); + var sp = services.BuildServiceProvider(); + var provider = sp.GetService(); + provider.Should().NotBeNull(); + provider.TaskId.Should().BeNull(); + provider.Action.Should().Be(action); + provider.Authorizer.Should().BeOfType(); + } + + [Fact] + public void AddTransientUserActionAuthorizerForAllActionsInTask_adds_IUserActinAuthorizerProvider_with_task_set() + { + // Arrange + string taskId = "Task_1"; + IServiceCollection services = new ServiceCollection(); + + // Act + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeFalse(); + services.AddTransientUserActionAuthorizerForAllActionsInTask(taskId); + + // Assert + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeTrue(); + services.IsAdded(typeof(UserActionAuthorizerStub)).Should().BeTrue(); + var sp = services.BuildServiceProvider(); + var provider = sp.GetService(); + provider.Should().NotBeNull(); + provider.TaskId.Should().Be(taskId); + provider.Action.Should().BeNull(); + provider.Authorizer.Should().BeOfType(); + } + + [Fact] + public void AddTransientUserActionAuthorizerForAllActionsInAllTasks_adds_IUserActinAuthorizerProvider_without_task_and_action_set() + { + // Arrange + IServiceCollection services = new ServiceCollection(); + + // Act + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeFalse(); + services.AddTransientUserActionAuthorizerForAllActionsInAllTasks(); + + // Assert + services.IsAdded(typeof(IUserActionAuthorizerProvider)).Should().BeTrue(); + services.IsAdded(typeof(UserActionAuthorizerStub)).Should().BeTrue(); + var sp = services.BuildServiceProvider(); + var provider = sp.GetService(); + provider.Should().NotBeNull(); + provider.TaskId.Should().BeNull(); + provider.Action.Should().BeNull(); + provider.Authorizer.Should().BeOfType(); + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs index 530784d12..21b92a900 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs @@ -319,12 +319,19 @@ public void GetFlowElement_returns_ProcessTask_with_id() } }, TaskType = "data", - DataTypesToSign = new() + SignatureConfiguration = new() { - "default", - "default2" - }, - SignatureDataType = "signature" + DataTypesToSign = new() + { + "default", + "default2" + }, + SignatureDataType = "signature", + UniqueFromSignaturesInDataTypes = new() + { + "signature1" + } + } } } }); diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn index 677c020af..057b32948 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn @@ -20,11 +20,16 @@ submit data - - default - default2 - - signature + + + default + default2 + + signature + + signature1 + + From b60e7ad1feeda07089a28a54c56f0d0734030802 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Mon, 26 Jun 2023 10:16:38 +0200 Subject: [PATCH 08/46] Implement UniqueSignatureAuthorizer to support checking for unique signatures (#266) * Implement UniqueSignatureAuthorizer to support checking for unique signatures across signing taks * trailing linebreaks * add tests and fix nullability * Add some more tests --- .../Extensions/ServiceCollectionExtensions.cs | 2 + .../Action/UniqueSignatureAuthorizer.cs | 79 ++++ .../signature-missing-signee-userid.json | 21 ++ .../TestData/signature-missing-signee.json | 17 + .../signature-signee-userid-null.json | 22 ++ .../Features/Action/TestData/signature.json | 22 ++ .../Action/UniqueSignatureAuthorizerTests.cs | 342 ++++++++++++++++++ 7 files changed, 505 insertions(+) create mode 100644 src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee-userid.json create mode 100644 test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee.json create mode 100644 test/Altinn.App.Core.Tests/Features/Action/TestData/signature-signee-userid-null.json create mode 100644 test/Altinn.App.Core.Tests/Features/Action/TestData/signature.json create mode 100644 test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index e4480613f..f621c0f82 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -27,6 +27,7 @@ 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.Profile; using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Internal.Secrets; @@ -243,6 +244,7 @@ private static void AddActionServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddHttpClient(); + services.AddTransientUserActionAuthorizerForActionInAllTasks("sign"); } } } diff --git a/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs b/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs new file mode 100644 index 000000000..2672f46a2 --- /dev/null +++ b/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs @@ -0,0 +1,79 @@ +using System.Text.Json; +using Altinn.App.Core.Extensions; +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.Process.Elements; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Action; + +/// +/// Implementation of IUserActionAuthorizer that checks if a signature is unique from other signatures defined in the list of dataTypes under uniqueFromSignaturesInDataTypes inside the bpmn file +/// +public class UniqueSignatureAuthorizer : IUserActionAuthorizer +{ + private readonly IAppMetadata _appMetadata; + private readonly IProcessReader _processReader; + private readonly IInstanceClient _instanceClient; + private readonly IDataClient _dataClient; + + /// + /// Intializes a new instance of the class + /// + /// The process reader + /// The instance client + /// The data client + /// The application metadata + public UniqueSignatureAuthorizer(IProcessReader processReader, IInstanceClient instanceClient, IDataClient dataClient, IAppMetadata appMetadata) + { + _processReader = processReader; + _instanceClient = instanceClient; + _dataClient = dataClient; + _appMetadata = appMetadata; + } + + /// + public async Task AuthorizeAction(UserActionAuthorizerContext context) + { + var flowElement = _processReader.GetFlowElement(context.TaskId) as ProcessTask; + if (flowElement?.ExtensionElements?.TaskExtension?.SignatureConfiguration?.UniqueFromSignaturesInDataTypes.Count > 0) + { + var appMetadata = await _appMetadata.GetApplicationMetadata(); + var instance = await _instanceClient.GetInstance(appMetadata.AppIdentifier.App, appMetadata.AppIdentifier.Org, context.InstanceIdentifier.InstanceOwnerPartyId, context.InstanceIdentifier.InstanceGuid); + var dataTypes = flowElement.ExtensionElements!.TaskExtension!.SignatureConfiguration!.UniqueFromSignaturesInDataTypes; + var signatureDataElements = instance.Data.Where(d => dataTypes.Contains(d.DataType)).ToList(); + foreach (var signatureDataElement in signatureDataElements) + { + var userId = await GetUserIdFromDataElementContainingSignDocument(appMetadata.AppIdentifier, context.InstanceIdentifier, signatureDataElement); + if (userId == context.User.GetUserOrOrgId()) + { + return false; + } + } + } + + return true; + } + + private async Task GetUserIdFromDataElementContainingSignDocument(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, DataElement dataElement) + { + await using var data = await _dataClient.GetBinaryData(appIdentifier.Org, appIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, Guid.Parse(dataElement.Id)); + try + { + JsonSerializerOptions options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }; + var signDocument = await JsonSerializer.DeserializeAsync(data, options); + return signDocument?.SigneeInfo.UserId ?? ""; + } + catch (JsonException) + { + return ""; + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee-userid.json b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee-userid.json new file mode 100644 index 000000000..078e5f77f --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee-userid.json @@ -0,0 +1,21 @@ +{ + "id": "061c0677-ea2a-4647-b754-42b08b986194", + "instanceGuid": "a32520b0-db9e-41a5-ac17-517474d9a0eb", + "signedTime": "2023-06-22T08:53:38.7521813Z", + "signeeInfo": { + "personNumber": "01039012345", + "organisationNumber": null + }, + "dataElementSignatures": [ + { + "dataElementId": "c4e56a1e-887e-411a-baaf-3ff9d71e9b52", + "sha256Hash": "b21b56007c4e0b05a94053ec046e20bad5af0949cf4c0761b9b4d6832b4bf22c", + "signed": true + }, + { + "dataElementId": "d076cc95-78b1-412b-942c-60559667b0f0", + "sha256Hash": "b098326ba147e5a17fcaddbf6bdb4c7262abb0978094300f8aaa8219435b669f", + "signed": true + } + ] +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee.json b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee.json new file mode 100644 index 000000000..4882d8ab7 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-missing-signee.json @@ -0,0 +1,17 @@ +{ + "id": "061c0677-ea2a-4647-b754-42b08b986194", + "instanceGuid": "a32520b0-db9e-41a5-ac17-517474d9a0eb", + "signedTime": "2023-06-22T08:53:38.7521813Z", + "dataElementSignatures": [ + { + "dataElementId": "c4e56a1e-887e-411a-baaf-3ff9d71e9b52", + "sha256Hash": "b21b56007c4e0b05a94053ec046e20bad5af0949cf4c0761b9b4d6832b4bf22c", + "signed": true + }, + { + "dataElementId": "d076cc95-78b1-412b-942c-60559667b0f0", + "sha256Hash": "b098326ba147e5a17fcaddbf6bdb4c7262abb0978094300f8aaa8219435b669f", + "signed": true + } + ] +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-signee-userid-null.json b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-signee-userid-null.json new file mode 100644 index 000000000..1efcf2a9f --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature-signee-userid-null.json @@ -0,0 +1,22 @@ +{ + "id": "061c0677-ea2a-4647-b754-42b08b986194", + "instanceGuid": "a32520b0-db9e-41a5-ac17-517474d9a0eb", + "signedTime": "2023-06-22T08:53:38.7521813Z", + "signeeInfo": { + "userId": null, + "personNumber": "01039012345", + "organisationNumber": null + }, + "dataElementSignatures": [ + { + "dataElementId": "c4e56a1e-887e-411a-baaf-3ff9d71e9b52", + "sha256Hash": "b21b56007c4e0b05a94053ec046e20bad5af0949cf4c0761b9b4d6832b4bf22c", + "signed": true + }, + { + "dataElementId": "d076cc95-78b1-412b-942c-60559667b0f0", + "sha256Hash": "b098326ba147e5a17fcaddbf6bdb4c7262abb0978094300f8aaa8219435b669f", + "signed": true + } + ] +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/TestData/signature.json b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature.json new file mode 100644 index 000000000..c2ddae8ea --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/TestData/signature.json @@ -0,0 +1,22 @@ +{ + "id": "061c0677-ea2a-4647-b754-42b08b986194", + "instanceGuid": "a32520b0-db9e-41a5-ac17-517474d9a0eb", + "signedTime": "2023-06-22T08:53:38.7521813Z", + "signeeInfo": { + "userId": "1337", + "personNumber": "01039012345", + "organisationNumber": null + }, + "dataElementSignatures": [ + { + "dataElementId": "c4e56a1e-887e-411a-baaf-3ff9d71e9b52", + "sha256Hash": "b21b56007c4e0b05a94053ec046e20bad5af0949cf4c0761b9b4d6832b4bf22c", + "signed": true + }, + { + "dataElementId": "d076cc95-78b1-412b-942c-60559667b0f0", + "sha256Hash": "b098326ba147e5a17fcaddbf6bdb4c7262abb0978094300f8aaa8219435b669f", + "signed": true + } + ] +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs b/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs new file mode 100644 index 000000000..e9e32789a --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs @@ -0,0 +1,342 @@ +#nullable enable +using System.Security.Claims; +using System.Text; +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Infrastructure.Clients.Storage; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.Base; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using AltinnCore.Authentication.Constants; +using FluentAssertions; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Action; + +public class UniqueSignatureAuthorizerTests : IDisposable +{ + private readonly Mock _processReaderMock; + private readonly Mock _instanceClientMock; + private readonly Mock _dataClientMock; + private readonly Mock _appMetadataMock; + + public UniqueSignatureAuthorizerTests() + { + _processReaderMock = new Mock(); + _instanceClientMock = new Mock(); + _dataClientMock = new Mock(); + _appMetadataMock = new Mock(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_uniqueFromSignaturesInDataTypes_not_defined() + { + ProcessElement processTask = new ProcessTask(); + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(new ClaimsPrincipal(), new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_uniqueFromSignaturesInDataTypes_null() + { + ProcessElement? processTask = null; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(new ClaimsPrincipal(), new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_SignatureConfiguration_is_null() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = null + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1000"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_TaskExtension_is_null() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = null + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1000"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_other_user_has_signed_previously() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1000"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + _instanceClientMock.Verify(i => i.GetInstance("xunit-app", "ttd", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"))); + _appMetadataMock.Verify(a => a.GetApplicationMetadata()); + _dataClientMock.Verify(d => d.GetBinaryData("ttd", "xunit-app", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"), Guid.Parse("ca62613c-f058-4899-b962-89dd6496a751"))); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_false_if_same_user_has_signed_previously() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + _instanceClientMock.Verify(i => i.GetInstance("xunit-app", "ttd", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"))); + _appMetadataMock.Verify(a => a.GetApplicationMetadata()); + _dataClientMock.Verify(d => d.GetBinaryData("ttd", "xunit-app", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"), Guid.Parse("ca62613c-f058-4899-b962-89dd6496a751"))); + result.Should().BeFalse(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_dataelement_not_of_type_SignDocument() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask, "signing-task-process.bpmn"); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + _instanceClientMock.Verify(i => i.GetInstance("xunit-app", "ttd", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"))); + _appMetadataMock.Verify(a => a.GetApplicationMetadata()); + _dataClientMock.Verify(d => d.GetBinaryData("ttd", "xunit-app", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"), Guid.Parse("ca62613c-f058-4899-b962-89dd6496a751"))); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_signdumcument_is_missing_signee() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask, "signature-missing-signee.json"); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + _instanceClientMock.Verify(i => i.GetInstance("xunit-app", "ttd", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"))); + _appMetadataMock.Verify(a => a.GetApplicationMetadata()); + _dataClientMock.Verify(d => d.GetBinaryData("ttd", "xunit-app", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"), Guid.Parse("ca62613c-f058-4899-b962-89dd6496a751"))); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_signdumcument_is_missing_signee_userid() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask, "signature-missing-signee-userid.json"); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + _instanceClientMock.Verify(i => i.GetInstance("xunit-app", "ttd", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"))); + _appMetadataMock.Verify(a => a.GetApplicationMetadata()); + _dataClientMock.Verify(d => d.GetBinaryData("ttd", "xunit-app", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"), Guid.Parse("ca62613c-f058-4899-b962-89dd6496a751"))); + result.Should().BeTrue(); + } + + [Fact] + public async Task AuthorizeAction_returns_true_if_signdumcument_signee_userid_is_null() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask, "signature-signee-userid-null.json"); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", "sign")); + _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); + _instanceClientMock.Verify(i => i.GetInstance("xunit-app", "ttd", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"))); + _appMetadataMock.Verify(a => a.GetApplicationMetadata()); + _dataClientMock.Verify(d => d.GetBinaryData("ttd", "xunit-app", 500001, Guid.Parse("abba2e90-f86f-4881-b0e8-38334408bcb4"), Guid.Parse("ca62613c-f058-4899-b962-89dd6496a751"))); + result.Should().BeTrue(); + } + + private UniqueSignatureAuthorizer CreateUniqueSignatureAuthorizer(ProcessElement? task, string signatureFileToRead = "signature.json") + { + _processReaderMock.Setup(sr => sr.GetFlowElement(It.IsAny())).Returns(task); + _appMetadataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata("ttd/xunit-app")); + _instanceClientMock.Setup(i => i.GetInstance(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new Instance() + { + Data = new List() + { + new() + { + DataType = "signature", + Id = "ca62613c-f058-4899-b962-89dd6496a751", + } + } + }); + FileStream fileStream = File.OpenRead(Path.Combine("Features", "Action", "TestData", signatureFileToRead)); + _dataClientMock.Setup(d => d.GetBinaryData(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(fileStream); + return new UniqueSignatureAuthorizer(_processReaderMock.Object, _instanceClientMock.Object, _dataClientMock.Object, _appMetadataMock.Object); + } + + public void Dispose() + { + _processReaderMock.VerifyNoOtherCalls(); + _instanceClientMock.VerifyNoOtherCalls(); + _dataClientMock.VerifyNoOtherCalls(); + _appMetadataMock.VerifyNoOtherCalls(); + } +} From 6e09641236c085803c63abd95232275f0d0c12d5 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Mon, 26 Jun 2023 12:01:17 +0200 Subject: [PATCH 09/46] Bugfix for missing taskId (#267) --- .../Controllers/ProcessController.cs | 4 +-- .../Action/UniqueSignatureAuthorizer.cs | 4 +++ .../Action/UniqueSignatureAuthorizerTests.cs | 31 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 90904fcd0..9f9eb47c6 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -276,7 +276,7 @@ public async Task> NextElement( bool authorized; string checkedAction = EnsureActionNotTaskType(processNext?.Action ?? altinnTaskType); - authorized = await AuthorizeAction(checkedAction, org, app, instanceOwnerPartyId, instanceGuid); + authorized = await AuthorizeAction(checkedAction, org, app, instanceOwnerPartyId, instanceGuid, instance.Process.CurrentTask?.ElementId); if (!authorized) { @@ -372,7 +372,7 @@ public async Task> CompleteProcess( { string altinnTaskType = EnsureActionNotTaskType(instance.Process.CurrentTask?.AltinnTaskType); - bool authorized = await AuthorizeAction(altinnTaskType, org, app, instanceOwnerPartyId, instanceGuid); + bool authorized = await AuthorizeAction(altinnTaskType, org, app, instanceOwnerPartyId, instanceGuid, instance.Process.CurrentTask?.ElementId); if (!authorized) { return Forbid(); diff --git a/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs b/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs index 2672f46a2..ff9aef12d 100644 --- a/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs +++ b/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs @@ -38,6 +38,10 @@ public UniqueSignatureAuthorizer(IProcessReader processReader, IInstanceClient i /// public async Task AuthorizeAction(UserActionAuthorizerContext context) { + if (context.TaskId == null) + { + return true; + } var flowElement = _processReader.GetFlowElement(context.TaskId) as ProcessTask; if (flowElement?.ExtensionElements?.TaskExtension?.SignatureConfiguration?.UniqueFromSignaturesInDataTypes.Count > 0) { diff --git a/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs b/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs index e9e32789a..e421722b8 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs @@ -172,6 +172,37 @@ public async Task AuthorizeAction_returns_false_if_same_user_has_signed_previous result.Should().BeFalse(); } + [Fact] + public async Task AuthorizeAction_returns_true_if_taskID_is_null() + { + ProcessElement processTask = new ProcessTask() + { + ExtensionElements = new() + { + TaskExtension = new() + { + SignatureConfiguration = new() + { + UniqueFromSignaturesInDataTypes = new() + { + "signature" + } + } + } + } + }; + UniqueSignatureAuthorizer authorizer = CreateUniqueSignatureAuthorizer(processTask); + var user = new ClaimsPrincipal(new ClaimsIdentity(new List() + { + new(AltinnCoreClaimTypes.UserId, "1337"), + new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), + new(AltinnCoreClaimTypes.Org, "tdd") + })); + + bool result = await authorizer.AuthorizeAction(new UserActionAuthorizerContext(user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), null, "sign")); + result.Should().BeTrue(); + } + [Fact] public async Task AuthorizeAction_returns_true_if_dataelement_not_of_type_SignDocument() { From 941dca86a3350c01a3c4aab4cf25e1e4bf239ded Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Thu, 6 Jul 2023 09:38:18 +0200 Subject: [PATCH 10/46] Fix typo in obsolete message --- src/Altinn.App.Core/Interface/IPersonRetriever.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Interface/IPersonRetriever.cs b/src/Altinn.App.Core/Interface/IPersonRetriever.cs index 6c99166ec..cf09d6e26 100644 --- a/src/Altinn.App.Core/Interface/IPersonRetriever.cs +++ b/src/Altinn.App.Core/Interface/IPersonRetriever.cs @@ -7,7 +7,7 @@ namespace Altinn.App.Core.Interface /// /// Describes the required methods for an implementation of a person repository client. /// - [Obsolete(message: "Use Altinn.App.Core.Internal.Register.IPersonClient instead", error: true)] + [Obsolete(message: "Use Altinn.App.Core.Internal.Registers.IPersonClient instead", error: true)] public interface IPersonRetriever { /// From cc2d8392734aef9b8b180adc87c0dd363f55c002 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Tue, 8 Aug 2023 13:09:46 +0200 Subject: [PATCH 11/46] Expose prometheus endpoint and som default metrics (#273) * Expose prometheus endpoint and som default metrics * Fix issues from CodeQL * Fix codesmells from sonar --- .../WebApplicationBuilderExtensions.cs | 46 +++ src/Altinn.App.Core/Altinn.App.Core.csproj | 2 + .../Extensions/ServiceCollectionExtensions.cs | 3 + .../Storage/InstanceClientMetricsDecorator.cs | 123 +++++++ .../Process/ProcessEngineMetricsDecorator.cs | 56 +++ .../InstanceClientMetricsDecoratorTests.cs | 345 ++++++++++++++++++ .../ProcessEngineMetricsDecoratorTests.cs | 304 +++++++++++++++ .../TestHelpers/PrometheusTestHelper.cs | 15 + 8 files changed, 894 insertions(+) create mode 100644 src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs create mode 100644 src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClientMetricsDecorator.cs create mode 100644 src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs create mode 100644 test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs create mode 100644 test/Altinn.App.Core.Tests/TestHelpers/PrometheusTestHelper.cs diff --git a/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs b/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs new file mode 100644 index 000000000..400dd87a8 --- /dev/null +++ b/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs @@ -0,0 +1,46 @@ +using System.Reflection; +using Altinn.App.Api.Helpers; +using Prometheus; + +namespace Altinn.App.Api.Extensions; + +/// +/// Altinn specific extensions for . +/// +public static class WebApplicationBuilderExtensions +{ + /// + /// Add default Altinn configuration for an app. + /// + /// The . + /// + public static IApplicationBuilder UseAltinnAppCommonConfiguration(this IApplicationBuilder app) + { + if (app is WebApplication webApp && webApp.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpMetrics(); + app.UseMetricServer(); + var appId = StartupHelper.GetApplicationId(); + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString(); + Metrics.DefaultRegistry.SetStaticLabels(new Dictionary() + { + { "application_id", appId }, + { "nuget_package_version", version } + }); + app.UseDefaultSecurityHeaders(); + app.UseRouting(); + app.UseStaticFiles('/' + appId); + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + app.UseHealthChecks("/health"); + return app; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index 7c88662f8..b6cb0d5e5 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -21,6 +21,8 @@ + + diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index f621c0f82..d4da8d756 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -45,6 +45,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Newtonsoft.Json.Linq; +using Prometheus; using IProcessEngine = Altinn.App.Core.Internal.Process.IProcessEngine; using IProcessReader = Altinn.App.Core.Internal.Process.IProcessReader; using ProcessEngine = Altinn.App.Core.Internal.Process.ProcessEngine; @@ -78,6 +79,7 @@ public static void AddPlatformServices(this IServiceCollection services, IConfig services.AddHttpClient(); services.AddHttpClient(); services.AddHttpClient(); + services.Decorate(); services.AddHttpClient(); services.AddHttpClient(); services.AddHttpClient(); @@ -231,6 +233,7 @@ private static void AddAppOptions(IServiceCollection services) private static void AddProcessServices(IServiceCollection services) { services.TryAddTransient(); + services.Decorate(); services.TryAddTransient(); services.TryAddSingleton(); services.TryAddTransient(); diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClientMetricsDecorator.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClientMetricsDecorator.cs new file mode 100644 index 000000000..a9d85ec54 --- /dev/null +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/InstanceClientMetricsDecorator.cs @@ -0,0 +1,123 @@ +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.Instances; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Primitives; +using Prometheus; + +namespace Altinn.App.Core.Infrastructure.Clients.Storage; + +/// +/// Decorator for the instance client that adds metrics for the number of instances created, completed and deleted. +/// +public class InstanceClientMetricsDecorator : IInstanceClient +{ + private readonly IInstanceClient _instanceClient; + private static readonly Counter InstancesCreatedCounter = Metrics.CreateCounter("altinn_app_instances_created", "Number of instances created", "result"); + private static readonly Counter InstancesCompletedCounter = Metrics.CreateCounter("altinn_app_instances_completed", "Number of instances completed", "result"); + private static readonly Counter InstancesDeletedCounter = Metrics.CreateCounter("altinn_app_instances_deleted", "Number of instances completed", "result", "mode" ); + + /// + /// Create a new instance of the class. + /// + /// The instance client to decorate. + public InstanceClientMetricsDecorator(IInstanceClient instanceClient) + { + _instanceClient = instanceClient; + } + + /// + public async Task GetInstance(string app, string org, int instanceOwnerPartyId, Guid instanceId) + { + return await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceId); + } + + /// + public async Task GetInstance(Instance instance) + { + return await _instanceClient.GetInstance(instance); + } + + /// + public async Task> GetInstances(Dictionary queryParams) + { + return await _instanceClient.GetInstances(queryParams); + } + + /// + public async Task UpdateProcess(Instance instance) + { + return await _instanceClient.UpdateProcess(instance); + } + + /// + public async Task CreateInstance(string org, string app, Instance instanceTemplate) + { + var success = false; + try + { + var instance = await _instanceClient.CreateInstance(org, app, instanceTemplate); + success = true; + return instance; + } + finally + { + InstancesCreatedCounter.WithLabels(success ? "success" : "failure").Inc(); + } + } + + /// + public async Task AddCompleteConfirmation(int instanceOwnerPartyId, Guid instanceGuid) + { + var success = false; + try + { + var instance = await _instanceClient.AddCompleteConfirmation(instanceOwnerPartyId, instanceGuid); + success = true; + return instance; + } + finally + { + InstancesCompletedCounter.WithLabels(success ? "success" : "failure").Inc(); + } + } + + /// + public async Task UpdateReadStatus(int instanceOwnerPartyId, Guid instanceGuid, string readStatus) + { + return await _instanceClient.UpdateReadStatus(instanceOwnerPartyId, instanceGuid, readStatus); + } + + /// + public async Task UpdateSubstatus(int instanceOwnerPartyId, Guid instanceGuid, Substatus substatus) + { + return await _instanceClient.UpdateSubstatus(instanceOwnerPartyId, instanceGuid, substatus); + } + + /// + public async Task UpdatePresentationTexts(int instanceOwnerPartyId, Guid instanceGuid, PresentationTexts presentationTexts) + { + return await _instanceClient.UpdatePresentationTexts(instanceOwnerPartyId, instanceGuid, presentationTexts); + } + + /// + public async Task UpdateDataValues(int instanceOwnerPartyId, Guid instanceGuid, DataValues dataValues) + { + return await _instanceClient.UpdateDataValues(instanceOwnerPartyId, instanceGuid, dataValues); + } + + /// + public async Task DeleteInstance(int instanceOwnerPartyId, Guid instanceGuid, bool hard) + { + var success = false; + try + { + var deleteInstance = await _instanceClient.DeleteInstance(instanceOwnerPartyId, instanceGuid, hard); + success = true; + return deleteInstance; + } + finally + { + InstancesDeletedCounter.WithLabels(success ? "success" : "failure", hard ? "hard" : "soft").Inc(); + } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs new file mode 100644 index 000000000..74a5000bd --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngineMetricsDecorator.cs @@ -0,0 +1,56 @@ +using Altinn.App.Core.Models.Process; +using Altinn.Platform.Storage.Interface.Models; +using Prometheus; + +namespace Altinn.App.Core.Internal.Process; + +/// +/// Decorator for the process engine that adds metrics for the number of processes started, ended and moved to next. +/// +public class ProcessEngineMetricsDecorator : IProcessEngine +{ + private readonly IProcessEngine _processEngine; + private static readonly Counter ProcessTaskStartCounter = Metrics.CreateCounter("altinn_app_process_start_count", "Number of tasks started", labelNames: "result" ); + private static readonly Counter ProcessTaskNextCounter = Metrics.CreateCounter("altinn_app_process_task_next_count", "Number of tasks moved to next", "result", "action", "task"); + private static readonly Counter ProcessTaskEndCounter = Metrics.CreateCounter("altinn_app_process_end_count", "Number of tasks ended", labelNames: "result"); + private static readonly Counter ProcessTimeCounter = Metrics.CreateCounter("altinn_app_process_end_time_total", "Number of seconds used to complete instances", labelNames: "result"); + + /// + /// Create a new instance of the class. + /// + /// The process engine to decorate. + public ProcessEngineMetricsDecorator(IProcessEngine processEngine) + { + _processEngine = processEngine; + } + + /// + public async Task StartProcess(ProcessStartRequest processStartRequest) + { + var result = await _processEngine.StartProcess(processStartRequest); + ProcessTaskStartCounter.WithLabels(result.Success ? "success" : "failure").Inc(); + return result; + } + + /// + public async Task Next(ProcessNextRequest request) + { + var result = await _processEngine.Next(request); + ProcessTaskNextCounter.WithLabels(result.Success ? "success" : "failure", request.Action?? "", request.Instance.Process?.CurrentTask?.ElementId ?? "").Inc(); + if(result.ProcessStateChange?.NewProcessState?.Ended != null) + { + ProcessTaskEndCounter.WithLabels(result.Success ? "success" : "failure").Inc(); + if (result.ProcessStateChange?.NewProcessState?.Started != null) + { + ProcessTimeCounter.WithLabels(result.Success ? "success" : "failure").Inc(result.ProcessStateChange.NewProcessState.Ended.Value.Subtract(result.ProcessStateChange.NewProcessState.Started.Value).TotalSeconds); + } + } + return result; + } + + /// + public async Task UpdateInstanceAndRerunEvents(ProcessStartRequest startRequest, List? events) + { + return await _processEngine.UpdateInstanceAndRerunEvents(startRequest, events); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs new file mode 100644 index 000000000..d2667de79 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs @@ -0,0 +1,345 @@ +using System.Net; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Infrastructure.Clients.Storage; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Tests.TestHelpers; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Primitives; +using Moq; +using Prometheus; +using Xunit; + +namespace Altinn.App.Core.Tests.InfrastrucZture.Clients.Storage; + +public class InstanceClientMetricsDecoratorTests +{ + public InstanceClientMetricsDecoratorTests() + { + Metrics.SuppressDefaultMetrics(); + } + + [Fact] + public async Task CreateInstance_calls_decorated_service_and_update_on_success() + { + // Arrange + Mock instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + var instanceTemplate = new Instance(); + + // Act + await instanceClientMetricsDecorator.CreateInstance("org", "app", instanceTemplate); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCount(1); + diff.Should().Contain("altinn_app_instances_created{result=\"success\"} 1"); + instanceClient.Verify(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task CreateInstance_calls_decorated_service_and_update_on_failure() + { + // Arrange + var instanceClient = new Mock(); + var platformHttpException = new PlatformHttpException(new HttpResponseMessage(HttpStatusCode.BadRequest), "test"); + instanceClient.Setup(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())).ThrowsAsync(platformHttpException); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + var instanceTemplate = new Instance(); + + // Act + var ex = await Assert.ThrowsAsync(async () => await instanceClientMetricsDecorator.CreateInstance("org", "app", instanceTemplate)); + ex.Should().BeSameAs(platformHttpException); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCount(1); + diff.Should().Contain("altinn_app_instances_created{result=\"failure\"} 1"); + instanceClient.Verify(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task AddCompleteConfirmation_calls_decorated_service_and_update_on_success() + { + // Arrange + Mock instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Act + await instanceClientMetricsDecorator.AddCompleteConfirmation(1337, Guid.NewGuid()); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCount(1); + diff.Should().Contain("altinn_app_instances_completed{result=\"success\"} 1"); + instanceClient.Verify(i => i.AddCompleteConfirmation(It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task AddCompleteConfirmation_calls_decorated_service_and_update_on_failure() + { + // Arrange + var instanceClient = new Mock(); + var platformHttpException = new PlatformHttpException(new HttpResponseMessage(HttpStatusCode.BadRequest), "test"); + instanceClient.Setup(i => i.AddCompleteConfirmation(It.IsAny(), It.IsAny())).ThrowsAsync(platformHttpException); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Act + var ex = await Assert.ThrowsAsync(async () => await instanceClientMetricsDecorator.AddCompleteConfirmation(1337, Guid.NewGuid())); + ex.Should().BeSameAs(platformHttpException); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCount(1); + diff.Should().Contain("altinn_app_instances_completed{result=\"failure\"} 1"); + instanceClient.Verify(i => i.AddCompleteConfirmation(It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task DeleteInstance_calls_decorated_service_and_update_on_success_soft_delete() + { + // Arrange + Mock instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Act + await instanceClientMetricsDecorator.DeleteInstance(1337, Guid.NewGuid(), false); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCount(1); + diff.Should().Contain("altinn_app_instances_deleted{result=\"success\",mode=\"soft\"} 1"); + instanceClient.Verify(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task DeleteInstance_calls_decorated_service_and_update_on_success_soft_hard() + { + // Arrange + Mock instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Act + await instanceClientMetricsDecorator.DeleteInstance(1337, Guid.NewGuid(), true); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCount(1); + diff.Should().Contain("altinn_app_instances_deleted{result=\"success\",mode=\"hard\"} 1"); + instanceClient.Verify(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task DeleteInstance_calls_decorated_service_and_update_on_failure() + { + // Arrange + var instanceClient = new Mock(); + var platformHttpException = new PlatformHttpException(new HttpResponseMessage(HttpStatusCode.BadRequest), "test"); + instanceClient.Setup(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())).ThrowsAsync(platformHttpException); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Act + var ex = await Assert.ThrowsAsync(async () => await instanceClientMetricsDecorator.DeleteInstance(1337, Guid.NewGuid(), false)); + ex.Should().BeSameAs(platformHttpException); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().HaveCount(1); + diff.Should().Contain("altinn_app_instances_deleted{result=\"failure\",mode=\"soft\"} 1"); + instanceClient.Verify(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetInstance_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Act + var instanceId = Guid.NewGuid(); + await instanceClientMetricsDecorator.GetInstance("test-app", "ttd", 1337, instanceId); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().BeEmpty(); + instanceClient.Verify(i => i.GetInstance("test-app", "ttd", 1337, instanceId)); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetInstance_instance_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + var instance = new Instance(); + + // Act + await instanceClientMetricsDecorator.GetInstance(instance); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().BeEmpty(); + instanceClient.Verify(i => i.GetInstance(instance)); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task GetInstances_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Act + await instanceClientMetricsDecorator.GetInstances(new Dictionary()); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().BeEmpty(); + instanceClient.Verify(i => i.GetInstances(new Dictionary())); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateProcess_of_instance_owner_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + var instance = new Instance(); + + // Act + await instanceClientMetricsDecorator.UpdateProcess(instance); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().BeEmpty(); + instanceClient.Verify(i => i.UpdateProcess(instance)); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateReadStatus_of_instance_owner_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + var instanceGuid = Guid.NewGuid(); + + // Act + await instanceClientMetricsDecorator.UpdateReadStatus(1337, instanceGuid, "read"); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().BeEmpty(); + instanceClient.Verify(i => i.UpdateReadStatus(1337, instanceGuid, "read")); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateSubstatus_of_instance_owner_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + var instanceGuid = Guid.NewGuid(); + var substatus = new Substatus(); + + // Act + await instanceClientMetricsDecorator.UpdateSubstatus(1337, instanceGuid, substatus); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().BeEmpty(); + instanceClient.Verify(i => i.UpdateSubstatus(1337, instanceGuid, substatus)); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdatePresentationTexts_of_instance_owner_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + var instanceGuid = Guid.NewGuid(); + var presentationTexts = new PresentationTexts(); + + // Act + await instanceClientMetricsDecorator.UpdatePresentationTexts(1337, instanceGuid, presentationTexts); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().BeEmpty(); + instanceClient.Verify(i => i.UpdatePresentationTexts(1337, instanceGuid, presentationTexts)); + instanceClient.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateDataValues_of_instance_owner_calls_decorated_service() + { + // Arrange + var instanceClient = new Mock(); + var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); + var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + var instanceGuid = Guid.NewGuid(); + var dataValues = new DataValues(); + + // Act + await instanceClientMetricsDecorator.UpdateDataValues(1337, instanceGuid, dataValues); + var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); + + // Assert + var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); + diff.Should().BeEmpty(); + instanceClient.Verify(i => i.UpdateDataValues(1337, instanceGuid, dataValues)); + instanceClient.VerifyNoOtherCalls(); + } + + private static List GetDiff(string s1, string s2) + { + List diff; + IEnumerable set1 = s1.Split('\n').Distinct().Where(s => !s.StartsWith("#")); + IEnumerable set2 = s2.Split('\n').Distinct().Where(s => !s.StartsWith("#")); + + diff = set2.Count() > set1.Count() ? set2.Except(set1).ToList() : set1.Except(set2).ToList(); + + return diff; + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs new file mode 100644 index 000000000..7ba00c9df --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineMetricsDecoratorTests.cs @@ -0,0 +1,304 @@ +using Altinn.App.Core.Internal.Process; +using Altinn.App.Core.Models.Process; +using Altinn.App.Core.Tests.TestHelpers; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Moq; +using Prometheus; +using Xunit; + +namespace Altinn.App.Core.Tests.Internal.Process; + +public class ProcessEngineMetricsDecoratorTests +{ + public ProcessEngineMetricsDecoratorTests() + { + Metrics.SuppressDefaultMetrics(); + } + + [Fact] + public async Task StartProcess_calls_decorated_service_and_increments_success_counter_when_successful() + { + // Arrange + var processEngine = new Mock(); + processEngine.Setup(p => p.StartProcess(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 = decorator.StartProcess(new ProcessStartRequest()); + + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_start_count{result=\"success\"} 1"); + result.Result.Success.Should().BeTrue(); + result = decorator.StartProcess(new ProcessStartRequest()); + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_start_count{result=\"success\"} 2"); + result.Result.Success.Should().BeTrue(); + processEngine.Verify(p => p.StartProcess(It.IsAny()), Times.Exactly(2)); + processEngine.VerifyNoOtherCalls(); + } + + [Fact] + public async Task StartProcess_calls_decorated_service_and_increments_failure_counter_when_unsuccessful() + { + // Arrange + var processEngine = new Mock(); + processEngine.Setup(p => p.StartProcess(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 = decorator.StartProcess(new ProcessStartRequest()); + + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_start_count{result=\"failure\"} 1"); + result.Result.Success.Should().BeFalse(); + result = decorator.StartProcess(new ProcessStartRequest()); + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_start_count{result=\"failure\"} 2"); + result.Result.Success.Should().BeFalse(); + processEngine.Verify(p => p.StartProcess(It.IsAny()), Times.Exactly(2)); + processEngine.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Next_calls_decorated_service_and_increments_success_counter_when_successful() + { + // Arrange + var processEngine = new Mock(); + processEngine.Setup(p => p.Next(It.IsAny())).ReturnsAsync(new ProcessChangeResult { Success = true }); + var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_task_next_count{result=\"success\",action=\"write\",task=\"Task_1\"}"); + + var result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }, + Action = "write" + }); + + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_task_next_count{result=\"success\",action=\"write\",task=\"Task_1\"} 1"); + result.Result.Success.Should().BeTrue(); + result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }, + Action = "write" + }); + var prometheusMetricsToString = await ReadPrometheusMetricsToString(); + prometheusMetricsToString.Should().Contain("altinn_app_process_task_next_count{result=\"success\",action=\"write\",task=\"Task_1\"} 2"); + result.Result.Success.Should().BeTrue(); + processEngine.Verify(p => p.Next(It.IsAny()), Times.Exactly(2)); + processEngine.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Next_calls_decorated_service_and_increments_failure_counter_when_unsuccessful() + { + // Arrange + var processEngine = new Mock(); + processEngine.Setup(p => p.Next(It.IsAny())).ReturnsAsync(new ProcessChangeResult { Success = false }); + var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_task_next_count{result=\"failure\",action=\"write\",task=\"Task_1\"}"); + + var result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }, + Action = "write" + }); + + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_task_next_count{result=\"failure\",action=\"write\",task=\"Task_1\"} 1"); + result.Result.Success.Should().BeFalse(); + result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_1" + } + } + }, + Action = "write" + }); + var prometheusMetricsToString = await ReadPrometheusMetricsToString(); + prometheusMetricsToString.Should().Contain("altinn_app_process_task_next_count{result=\"failure\",action=\"write\",task=\"Task_1\"} 2"); + result.Result.Success.Should().BeFalse(); + processEngine.Verify(p => p.Next(It.IsAny()), Times.Exactly(2)); + processEngine.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Next_calls_decorated_service_and_increments_success_and_end_counters_when_successful_and_process_ended() + { + // Arrange + var processEngine = new Mock(); + var ended = DateTime.Now; + var started = ended.AddSeconds(-20); + processEngine.Setup(p => p.Next(It.IsAny())).ReturnsAsync(new ProcessChangeResult + { + Success = true, + ProcessStateChange = new() + { + NewProcessState = new() + { + Ended = ended, + Started = started + } + } + }); + var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_task_next_count{result=\"success\",action=\"confirm\",task=\"Task_2\"}"); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_end_count{result=\"success\"}"); + (await ReadPrometheusMetricsToString()).Should().NotContain("altinn_app_process_end_time_total{result=\"success\"}"); + + var result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_2" + } + } + }, + Action = "confirm" + }); + + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_task_next_count{result=\"success\",action=\"confirm\",task=\"Task_2\"} 1"); + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_end_count{result=\"success\"} 1"); + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_end_time_total{result=\"success\"} 20"); + result.Result.Success.Should().BeTrue(); + result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_2" + } + } + }, + Action = "confirm" + }); + var prometheusMetricsToString = await ReadPrometheusMetricsToString(); + prometheusMetricsToString.Should().Contain("altinn_app_process_task_next_count{result=\"success\",action=\"confirm\",task=\"Task_2\"} 2"); + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_end_count{result=\"success\"} 2"); + (await ReadPrometheusMetricsToString()).Should().Contain("altinn_app_process_end_time_total{result=\"success\"} 40"); + result.Result.Success.Should().BeTrue(); + processEngine.Verify(p => p.Next(It.IsAny()), Times.Exactly(2)); + processEngine.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Next_calls_decorated_service_and_increments_failure_and_end_counters_when_unsuccessful_and_process_ended_no_time_added_if_started_null() + { + // Arrange + var processEngine = new Mock(); + var ended = DateTime.Now; + processEngine.Setup(p => p.Next(It.IsAny())).ReturnsAsync(new ProcessChangeResult + { + Success = false, + ProcessStateChange = new() + { + NewProcessState = new() + { + Ended = ended + } + } + }); + var decorator = new ProcessEngineMetricsDecorator(processEngine.Object); + var prometheusMetricsToString = await ReadPrometheusMetricsToString(); + prometheusMetricsToString.Should().NotContain("altinn_app_process_task_next_count{result=\"failure\",action=\"confirm\",task=\"Task_3\"}"); + prometheusMetricsToString.Should().NotContain("altinn_app_process_end_count{result=\"failure\"}"); + prometheusMetricsToString.Should().NotContain("altinn_app_process_end_time_total{result=\"failure\"}"); + + var result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_3" + } + } + }, + Action = "confirm" + }); + + prometheusMetricsToString = await ReadPrometheusMetricsToString(); + prometheusMetricsToString.Should().Contain("altinn_app_process_task_next_count{result=\"failure\",action=\"confirm\",task=\"Task_3\"} 1"); + prometheusMetricsToString.Should().Contain("altinn_app_process_end_count{result=\"failure\"} 1"); + prometheusMetricsToString.Should().NotContain("altinn_app_process_end_time_total{result=\"failure\"}"); + result.Result.Success.Should().BeFalse(); + result = decorator.Next(new ProcessNextRequest() + { + Instance = new() + { + Process = new() + { + CurrentTask = new() + { + ElementId = "Task_3" + } + } + }, + Action = "confirm" + }); + prometheusMetricsToString = await ReadPrometheusMetricsToString(); + prometheusMetricsToString.Should().Contain("altinn_app_process_task_next_count{result=\"failure\",action=\"confirm\",task=\"Task_3\"} 2"); + prometheusMetricsToString.Should().Contain("altinn_app_process_end_count{result=\"failure\"} 2"); + prometheusMetricsToString.Should().NotContain("altinn_app_process_end_time_total{result=\"failure\"}"); + result.Result.Success.Should().BeFalse(); + processEngine.Verify(p => p.Next(It.IsAny()), Times.Exactly(2)); + processEngine.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateInstanceAndRerunEvents_calls_decorated_service() + { + // Arrange + var processEngine = new Mock(); + processEngine.Setup(p => p.UpdateInstanceAndRerunEvents(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()); + + processEngine.Verify(p => p.UpdateInstanceAndRerunEvents(It.IsAny(), It.IsAny>()), Times.Once); + processEngine.VerifyNoOtherCalls(); + } + + private static async Task ReadPrometheusMetricsToString() + { + return await PrometheusTestHelper.ReadPrometheusMetricsToString(); + } +} diff --git a/test/Altinn.App.Core.Tests/TestHelpers/PrometheusTestHelper.cs b/test/Altinn.App.Core.Tests/TestHelpers/PrometheusTestHelper.cs new file mode 100644 index 000000000..71b5d93b6 --- /dev/null +++ b/test/Altinn.App.Core.Tests/TestHelpers/PrometheusTestHelper.cs @@ -0,0 +1,15 @@ +using Prometheus; + +namespace Altinn.App.Core.Tests.TestHelpers; + +public class PrometheusTestHelper +{ + public static async Task ReadPrometheusMetricsToString() + { + MemoryStream memoryStream = new MemoryStream(); + await Metrics.DefaultRegistry.CollectAndExportAsTextAsync(memoryStream); + using StreamReader reader = new StreamReader(memoryStream); + memoryStream.Position = 0; + return reader.ReadToEnd(); + } +} \ No newline at end of file From c40e14cb84b571952981940f5871b6a349f3ad0d Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Tue, 8 Aug 2023 14:57:36 +0200 Subject: [PATCH 12/46] merge main into v8 (#284) * Remove trailing slash from source filter in event subscription. (#242) Co-authored-by: Ronny Birkeli * Fix unauthorized submit resulting in 500 (#243) * catch PlatformException and return 403 * remove code smell * add test for 403 case * add test that other PlatformHttpExceptions are rethrown * return ok inside try block * chore(deps): update nuget non-major dependencies (#251) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Support for extended file analysis (#229) Co-authored-by: Ronny Birkeli * chore(deps): update dependency coverlet.collector to v6 (#252) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Documentation of parameteres used when generating codelists (#259) Co-authored-by: Ronny Birkeli * chore(deps): update nuget non-major dependencies (#254) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency jwtcookieauthentication to v3 (#258) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update nuget non-major dependencies (#270) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ronny Birkeli * Feat/add instance selection options to appmetadata (#271) * Add instance selection option to appmetadata * update test to use new OnEntry class * Fix obsolete method in IAppResources * newlines cleanup --------- Co-authored-by: Vemund Gaukstad * Add defaultRowsPerPage option to InstanceSelection (#272) * add defaultRowsPerPage option to InstanceSelection * remove pagination class * chore(deps): update nuget non-major dependencies (#274) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Mark onEntry as nullable and add check for null (#279) * Introduce new option for default selected pages per row (#280) * Introduce new option for default selected pages per row * fix codesmells * chore(deps): update nuget non-major dependencies (#281) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fixed missing await statement (#282) * Fixed missing await statement * Changed from array to string --------- Co-authored-by: Ronny Birkeli * Fix bug in 7.13.0 where anonymous statlessapps does not allow anonymous users even if configured (#283) * Sync backend expression functions with frontend (#277) * Implement missing functions in backend * add some testcases for functions that should be evaluated before implemented in the backend code * Simplify some functions * last sync changes from main * update some testfiles * fix some codesmells --------- Co-authored-by: Ronny Birkeli Co-authored-by: Ronny Birkeli Co-authored-by: Magnus Revheim Martinsen Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Altinn.App.Api/Altinn.App.Api.csproj | 5 +- .../Controllers/DataController.cs | 86 +++- .../Controllers/HomeController.cs | 6 +- .../Controllers/ValidateController.cs | 15 +- .../DataRestrictionValidation.cs | 106 +++-- src/Altinn.App.Core/Altinn.App.Core.csproj | 10 +- .../Extensions/HttpContextExtensions.cs | 28 ++ .../Extensions/ServiceCollectionExtensions.cs | 15 + src/Altinn.App.Core/Features/FeatureFlags.cs | 6 + .../FileAnalyzis/FileAnalyserFactory.cs | 29 ++ .../FileAnalyzis/FileAnalysisResult.cs | 44 ++ .../FileAnalyzis/FileAnalysisService.cs | 44 ++ .../Features/FileAnalyzis/IFileAnalyser.cs | 20 + .../FileAnalyzis/IFileAnalyserFactory.cs | 16 + .../FileAnalyzis/IFileAnalysisService.cs | 19 + .../Validation/FileValidationService.cs | 44 ++ .../Validation/FileValidatorFactory.cs | 28 ++ .../Validation/IFileValidationService.cs | 17 + .../Features/Validation/IFileValidator.cs | 22 + .../Validation/IFileValidatorFactory.cs | 15 + .../Helpers/ShadowFieldsConverter.cs | 5 +- .../Implementation/AppResourcesSI.cs | 12 +- .../Events/EventsSubscriptionClient.cs | 2 +- .../Clients/Storage/DataClient.cs | 17 +- .../Internal/App/FrontendFeatures.cs | 14 +- .../Expressions/ExpressionEvaluator.cs | 159 ++++++- src/Altinn.App.Core/Models/AppOptions.cs | 11 +- .../Models/ApplicationMetadata.cs | 6 + .../Expressions/ExpressionFunctionEnum.cs | 36 ++ .../Models/InstanceSelection.cs | 40 ++ src/Altinn.App.Core/Models/OnEntry.cs | 17 + .../Models/Validation/ValidationIssue.cs | 22 +- .../Models/Validation/ValidationIssueCodes.cs | 10 + .../Altinn.App.Api.Tests.csproj | 13 +- .../Controllers/DataControllerTests.cs | 178 ++++++++ .../Controllers/FileScanControllerTests.cs | 1 - .../Controllers/ValidateControllerTests.cs | 91 +++- .../CustomWebApplicationFactory.cs | 54 +++ ...3-fe31-4ef5-8fb9-dd3f479354cd.pretest.json | 39 ++ ...3-fe31-4ef5-8fb9-dd3f479354ce.pretest.json | 39 ++ test/Altinn.App.Api.Tests/Data/TestData.cs | 148 ++++++- .../_testdata_/example.jpg.pdf | Bin 0 -> 63943 bytes .../_testdata_/example.pdf | Bin 0 -> 17831 bytes .../contributer-restriction/appsettings.json | 34 ++ .../config/applicationmetadata.json | 47 +++ .../config/authorization/policy.xml | 253 +++++++++++ .../config/process/process.bpmn | 45 ++ .../config/texts/resource.nb.json | 169 ++++++++ .../eformidling-app/appsettings.json | 0 .../config/applicationmetadata.json | 0 .../config/authorization/policy.xml | 0 .../config/process/process.bpmn | 0 .../config/texts/resource.nb.json | 0 .../{ttd => tdd}/eformidling-app/logic/App.cs | 16 +- .../logic/EFormidlingMetadata.cs | 0 .../eformidling-app/models/Skjema.cs | 0 .../eformidling-app/models/Skjema.xsd | 0 .../eformidling-app/ui/RuleHandler.js | 0 .../ui/layouts/FormLayout.json | 0 ...EformidlingStatusCheckEventHandlerTests.cs | 107 +++-- .../DataRestrictionValidationTests.cs | 113 ++--- .../Mocks/AppMetadataMock.cs | 143 +++++++ .../Mocks/DataClientMock.cs | 392 ++++++++++++++++++ .../Mocks/InstanceClientMockSi.cs | 19 +- test/Altinn.App.Api.Tests/Program.cs | 5 + .../Utils/PrincipalUtil.cs | 17 + .../Altinn.App.Common.Tests.csproj | 8 +- .../Altinn.App.Core.Tests.csproj | 10 +- .../Features/Action/SigningUserActionTests.cs | 4 +- .../Implementation/AppResourcesSITests.cs | 57 ++- .../Implementation/DefaultTaskEventsTests.cs | 2 +- .../no-on-entry.applicationmetadata.json | 27 ++ .../Internal/App/AppMedataTest.cs | 231 ++++++++++- .../Internal/App/FrontendFeaturesTest.cs | 27 +- ...acy-selectoptions.applicationmetadata.json | 35 ++ ...new-selectoptions.applicationmetadata.json | 35 ++ ...new-selectoptions.applicationmetadata.json | 36 ++ .../Internal/Pdf/TestDoubles/Skjema.cs | 1 + .../CommonTests/TestFunctions.cs | 36 ++ .../context-lists/groups/noData.json | 2 +- .../context-lists/groups/oneRow.json | 2 +- .../context-lists/groups/twoRows.json | 2 +- .../nonRepeatingGroups/maxCount0.json | 2 +- .../nonRepeatingGroups/maxCount1.json | 2 +- .../nonRepeatingGroups/simple.json | 2 +- .../recursiveGroups/recursiveNoData.json | 2 +- .../recursiveGroups/recursiveOneRow.json | 2 +- .../recursiveTwoRowsInner.json | 2 +- .../recursiveTwoRowsOuter.json | 2 +- .../context-lists/simple/twoPages.json | 2 +- .../functions/commaContains/empty-string.json | 5 + .../functions/commaContains/null.json | 5 + .../functions/commaContains/null2.json | 5 + .../should-include-word-in-string.json | 5 + .../should-not-include-word-in-string.json | 5 + .../string-list-include-number.json | 5 + .../component/distant-across-page.json | 82 ++-- .../functions/component/distant.json | 68 +-- .../functions/contains/case-sensitivity.json | 5 + .../functions/contains/empty-string.json | 5 + .../functions/contains/exact-match.json | 5 + .../functions/contains/not-match.json | 5 + .../shared-tests/functions/contains/null.json | 5 + .../functions/contains/null2.json | 5 + .../functions/contains/null3.json | 5 + .../functions/contains/null4.json | 5 + .../functions/contains/partial-match.json | 5 + .../functions/endsWith/case-sensitivity.json | 5 + .../functions/endsWith/empty-string.json | 5 + .../functions/endsWith/ends-with-null.json | 5 + .../functions/endsWith/ends-with-number.json | 5 + .../functions/endsWith/ends-with.json | 5 + .../functions/endsWith/exact-match.json | 5 + .../functions/endsWith/not-ends-with.json | 5 + .../endsWith/number-ends-with-number.json | 5 + ...lowercase-number-should-return-string.json | 5 + .../functions/lowerCase/null.json | 5 + .../lowerCase/should-lowercase-string.json | 5 + .../should-lowercase-whole-string.json | 5 + .../should-lowercase-whole-word.json | 5 + .../notContains/case-sensitivity.json | 5 + .../functions/notContains/exact-match.json | 5 + .../functions/notContains/not-match.json | 5 + .../functions/notContains/null.json | 5 + .../functions/notContains/null2.json | 5 + .../functions/notContains/null3.json | 5 + .../functions/notContains/null4.json | 5 + ...arest-integer-with-decimal-as-strings.json | 5 + .../round/nearest-integer-with-decimal.json | 5 + .../functions/round/nearest-integer.json | 5 + .../round/round-0-decimal-places.json | 5 + .../round/round-0-decimal-places2.json | 5 + .../round/round-negative-number.json | 5 + .../functions/round/round-null.json | 5 + .../functions/round/round-null2.json | 5 + .../functions/round/round-strings.json | 5 + .../round/round-with-too-many-args.json | 5 + .../round/round-without-decimalCount.json | 5 + .../round/round-without-decimalCount2.json | 5 + .../startsWith/case-sensitivity.json | 5 + .../functions/startsWith/empty-string.json | 5 + .../functions/startsWith/exact-match.json | 5 + .../functions/startsWith/not-starts-with.json | 5 + .../functions/startsWith/null.json | 5 + .../functions/startsWith/null2.json | 5 + .../startsWith/number-starts-with-number.json | 5 + .../startsWith/number-starts-with-string.json | 5 + .../startsWith/start-with-number.json | 5 + .../functions/startsWith/start-with.json | 5 + .../functions/stringLength/empty-string.json | 5 + .../stringLength/length-of-number.json | 5 + .../stringLength/length-of-unicode.json | 5 + .../functions/stringLength/null.json | 5 + .../functions/stringLength/string-length.json | 5 + .../stringLength/whitespace-length.json | 5 + .../functions/upperCase/null.json | 5 + .../upperCase/should-uppercase-string.json | 5 + .../should-uppercase-whole-string.json | 5 + .../should-uppercase-whole-word.json | 5 + ...uppercase-number-should-return-string.json | 5 + .../functions/authContext/read-confirm.json | 15 + .../authContext/read-sign-reject.json | 15 + .../functions/authContext/read-sign.json | 20 + .../functions/authContext/read-write.json | 15 + .../language/should-return-nb-if-not-set.json | 5 + ...ld-return-profile-settings-preference.json | 8 + .../should-return-selected-language.json | 5 + .../functions/text/null.json | 5 + ...ould-return-key-name-if-key-not-exist.json | 5 + ...ariable-in-rep-group-no-index-markers.json | 114 +++++ ...t-resource-with-variable-in-rep-group.json | 114 +++++ ...ld-return-text-resource-with-variable.json | 25 ++ .../text/should-return-text-resource.json | 11 + .../Options/AppOptionsFactoryTests.cs | 16 +- 174 files changed, 3877 insertions(+), 366 deletions(-) create mode 100644 src/Altinn.App.Core/Extensions/HttpContextExtensions.cs create mode 100644 src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs create mode 100644 src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs create mode 100644 src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs create mode 100644 src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs create mode 100644 src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs create mode 100644 src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs create mode 100644 src/Altinn.App.Core/Features/Validation/FileValidationService.cs create mode 100644 src/Altinn.App.Core/Features/Validation/FileValidatorFactory.cs create mode 100644 src/Altinn.App.Core/Features/Validation/IFileValidationService.cs create mode 100644 src/Altinn.App.Core/Features/Validation/IFileValidator.cs create mode 100644 src/Altinn.App.Core/Features/Validation/IFileValidatorFactory.cs create mode 100644 src/Altinn.App.Core/Models/InstanceSelection.cs create mode 100644 src/Altinn.App.Core/Models/OnEntry.cs create mode 100644 test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs create mode 100644 test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/1337/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd.pretest.json create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/1337/1fc98a23-fe31-4ef5-8fb9-dd3f479354ce.pretest.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/_testdata_/example.jpg.pdf create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/_testdata_/example.pdf create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/appsettings.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/authorization/policy.xml create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/process/process.bpmn create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/texts/resource.nb.json rename test/Altinn.App.Api.Tests/Data/apps/{ttd => tdd}/eformidling-app/appsettings.json (100%) rename test/Altinn.App.Api.Tests/Data/apps/{ttd => tdd}/eformidling-app/config/applicationmetadata.json (100%) rename test/Altinn.App.Api.Tests/Data/apps/{ttd => tdd}/eformidling-app/config/authorization/policy.xml (100%) rename test/Altinn.App.Api.Tests/Data/apps/{ttd => tdd}/eformidling-app/config/process/process.bpmn (100%) rename test/Altinn.App.Api.Tests/Data/apps/{ttd => tdd}/eformidling-app/config/texts/resource.nb.json (100%) rename test/Altinn.App.Api.Tests/Data/apps/{ttd => tdd}/eformidling-app/logic/App.cs (59%) rename test/Altinn.App.Api.Tests/Data/apps/{ttd => tdd}/eformidling-app/logic/EFormidlingMetadata.cs (100%) rename test/Altinn.App.Api.Tests/Data/apps/{ttd => tdd}/eformidling-app/models/Skjema.cs (100%) rename test/Altinn.App.Api.Tests/Data/apps/{ttd => tdd}/eformidling-app/models/Skjema.xsd (100%) rename test/Altinn.App.Api.Tests/Data/apps/{ttd => tdd}/eformidling-app/ui/RuleHandler.js (100%) rename test/Altinn.App.Api.Tests/Data/apps/{ttd => tdd}/eformidling-app/ui/layouts/FormLayout.json (100%) create mode 100644 test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs create mode 100644 test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs create mode 100644 test/Altinn.App.Core.Tests/Implementation/TestData/AppMetadata/no-on-entry.applicationmetadata.json create mode 100644 test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/onentry-legacy-selectoptions.applicationmetadata.json create mode 100644 test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/onentry-new-selectoptions.applicationmetadata.json create mode 100644 test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/onentry-prefer-new-selectoptions.applicationmetadata.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/empty-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/null.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/null2.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/should-include-word-in-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/should-not-include-word-in-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/string-list-include-number.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/case-sensitivity.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/empty-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/exact-match.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/not-match.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null2.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null3.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null4.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/partial-match.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/case-sensitivity.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/empty-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/ends-with-null.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/ends-with-number.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/ends-with.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/exact-match.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/not-ends-with.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/number-ends-with-number.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/lowercase-number-should-return-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/null.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/should-lowercase-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/should-lowercase-whole-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/should-lowercase-whole-word.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/case-sensitivity.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/exact-match.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/not-match.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null2.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null3.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null4.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/nearest-integer-with-decimal-as-strings.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/nearest-integer-with-decimal.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/nearest-integer.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-0-decimal-places.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-0-decimal-places2.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-negative-number.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-null.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-null2.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-strings.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-with-too-many-args.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-without-decimalCount.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-without-decimalCount2.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/case-sensitivity.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/empty-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/exact-match.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/not-starts-with.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/null.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/null2.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/number-starts-with-number.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/number-starts-with-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/start-with-number.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/start-with.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/empty-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/length-of-number.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/length-of-unicode.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/null.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/string-length.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/whitespace-length.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/null.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/should-uppercase-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/should-uppercase-whole-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/should-uppercase-whole-word.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/uppercase-number-should-return-string.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-confirm.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-sign-reject.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-sign.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-write.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/language/should-return-nb-if-not-set.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/language/should-return-profile-settings-preference.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/language/should-return-selected-language.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/null.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-key-name-if-key-not-exist.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource-with-variable-in-rep-group-no-index-markers.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource-with-variable-in-rep-group.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource-with-variable.json create mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource.json diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index c6df17536..c1553681f 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -20,12 +20,13 @@ - + + - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index 208e6063b..84560ece2 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -5,6 +5,9 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; +using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Features.FileAnalyzis; +using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Serialization; using Altinn.App.Core.Internal.App; @@ -13,10 +16,12 @@ using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Prefill; using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; +using Microsoft.FeatureManagement; using Microsoft.Net.Http.Headers; namespace Altinn.App.Api.Controllers @@ -38,7 +43,9 @@ public class DataController : ControllerBase private readonly IAppResources _appResourcesService; private readonly IAppMetadata _appMetadata; private readonly IPrefill _prefillService; - + private readonly IFileAnalysisService _fileAnalyserService; + private readonly IFileValidationService _fileValidationService; + private readonly IFeatureManager _featureManager; private const long REQUEST_SIZE_LIMIT = 2000 * 1024 * 1024; /// @@ -52,7 +59,10 @@ public class DataController : ControllerBase /// Service for generating app model /// The apps resource service /// The app metadata service + /// The feature manager controlling enabled features. /// A service with prefill related logic. + /// Service used to analyse files uploaded. + /// Service used to validate files uploaded. public DataController( ILogger logger, IInstanceClient instanceClient, @@ -61,8 +71,11 @@ public DataController( IDataProcessor dataProcessor, IAppModel appModel, IAppResources appResourcesService, + IPrefill prefillService, + IFileAnalysisService fileAnalyserService, + IFileValidationService fileValidationService, IAppMetadata appMetadata, - IPrefill prefillService) + IFeatureManager featureManager) { _logger = logger; @@ -74,6 +87,9 @@ public DataController( _appResourcesService = appResourcesService; _appMetadata = appMetadata; _prefillService = prefillService; + _fileAnalyserService = fileAnalyserService; + _fileValidationService = fileValidationService; + _featureManager = featureManager; } /// @@ -139,12 +155,40 @@ public async Task Create( } else { - if (!DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataTypeFromMetadata, out ActionResult errorResponse)) + (bool validationRestrictionSuccess, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataTypeFromMetadata); + if (!validationRestrictionSuccess) + { + return new BadRequestObjectResult(await GetErrorDetails(errors)); + } + + StreamContent streamContent = Request.CreateContentStream(); + + using Stream fileStream = new MemoryStream(); + await streamContent.CopyToAsync(fileStream); + + bool parseSuccess = Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues); + string filename = parseSuccess ? DataRestrictionValidation.GetFileNameFromHeader(headerValues) : string.Empty; + + IEnumerable fileAnalysisResults = new List(); + if (FileAnalysisEnabledForDataType(dataTypeFromMetadata)) + { + fileAnalysisResults = await _fileAnalyserService.Analyse(dataTypeFromMetadata, fileStream, filename); + } + + bool fileValidationSuccess = true; + List validationIssues = new(); + if (FileValidationEnabledForDataType(dataTypeFromMetadata)) + { + (fileValidationSuccess, validationIssues) = await _fileValidationService.Validate(dataTypeFromMetadata, fileAnalysisResults); + } + + if (!fileValidationSuccess) { - return errorResponse; + return new BadRequestObjectResult(await GetErrorDetails(validationIssues)); } - return await CreateBinaryData(org, app, instance, dataType); + fileStream.Seek(0, SeekOrigin.Begin); + return await CreateBinaryData(instance, dataType, streamContent.Headers.ContentType.ToString(), filename, fileStream); } } catch (PlatformHttpException e) @@ -153,6 +197,29 @@ public async Task Create( } } + /// + /// File validation requires json object in response and is introduced in the + /// the methods above validating files. In order to be consistent for the return types + /// of this controller, old methods are updated to return json object in response. + /// Since this is a breaking change, a feature flag is introduced to control the behaviour, + /// and the developer need to opt-in to the new behaviour. Json object are by default + /// returned as part of file validation which is a new feature. + /// + private async Task GetErrorDetails(List errors) + { + return await _featureManager.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse) ? errors : string.Join(";", errors.Select(x => x.Description)); + } + + private static bool FileAnalysisEnabledForDataType(DataType dataTypeFromMetadata) + { + return dataTypeFromMetadata.EnabledFileAnalysers != null && dataTypeFromMetadata.EnabledFileAnalysers.Count > 0; + } + + private static bool FileValidationEnabledForDataType(DataType dataTypeFromMetadata) + { + return dataTypeFromMetadata.EnabledFileValidators != null && dataTypeFromMetadata.EnabledFileValidators.Count > 0; + } + /// /// Gets a data element from storage and applies business logic if nessesary. /// @@ -262,9 +329,10 @@ public async Task Put( } DataType dataTypeFromMetadata = (await _appMetadata.GetApplicationMetadata()).DataTypes.FirstOrDefault(e => e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase)); - if (!DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataTypeFromMetadata, out ActionResult errorResponse)) + (bool validationRestrictionSuccess, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataTypeFromMetadata); + if (!validationRestrictionSuccess) { - return errorResponse; + return new BadRequestObjectResult(await GetErrorDetails(errors)); } return await PutBinaryData(instanceOwnerPartyId, instanceGuid, dataGuid); @@ -355,12 +423,12 @@ private ActionResult ExceptionResponse(Exception exception, string message) return StatusCode(500, $"{message}"); } - private async Task CreateBinaryData(string org, string app, Instance instanceBefore, string dataType) + private async Task CreateBinaryData(Instance instanceBefore, string dataType, string contentType, string filename, Stream fileStream) { int instanceOwnerPartyId = int.Parse(instanceBefore.Id.Split("/")[0]); Guid instanceGuid = Guid.Parse(instanceBefore.Id.Split("/")[1]); - DataElement dataElement = await _dataClient.InsertBinaryData(org, app, instanceOwnerPartyId, instanceGuid, dataType, Request); + DataElement dataElement = await _dataClient.InsertBinaryData(instanceBefore.Id, dataType, contentType, filename, fileStream); if (Guid.Parse(dataElement.Id) == Guid.Empty) { diff --git a/src/Altinn.App.Api/Controllers/HomeController.cs b/src/Altinn.App.Api/Controllers/HomeController.cs index 2670a3902..282a26f8a 100644 --- a/src/Altinn.App.Api/Controllers/HomeController.cs +++ b/src/Altinn.App.Api/Controllers/HomeController.cs @@ -104,7 +104,7 @@ private async Task ShouldShowAppView() return true; } - Application application = await _appMetadata.GetApplicationMetadata(); + ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); if (!IsStatelessApp(application)) { return false; @@ -120,7 +120,7 @@ private async Task ShouldShowAppView() return false; } - private bool IsStatelessApp(Application application) + private bool IsStatelessApp(ApplicationMetadata application) { if (application?.OnEntry == null) { @@ -130,7 +130,7 @@ private bool IsStatelessApp(Application application) return !_onEntryWithInstance.Contains(application.OnEntry.Show); } - private DataType? GetStatelessDataType(Application application) + private DataType? GetStatelessDataType(ApplicationMetadata application) { string layoutSetsString = _appResources.GetLayoutSets(); JsonSerializerOptions options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; diff --git a/src/Altinn.App.Api/Controllers/ValidateController.cs b/src/Altinn.App.Api/Controllers/ValidateController.cs index b6237b409..4d67fc1ba 100644 --- a/src/Altinn.App.Api/Controllers/ValidateController.cs +++ b/src/Altinn.App.Api/Controllers/ValidateController.cs @@ -62,9 +62,20 @@ public async Task ValidateInstance( throw new ValidationException("Unable to validate instance without a started process."); } - List messages = await _validationService.ValidateAndUpdateProcess(instance, taskId); + try + { + List messages = await _validationService.ValidateAndUpdateProcess(instance, taskId); + return Ok(messages); + } + catch (PlatformHttpException exception) + { + if (exception.Response.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + return StatusCode(403); + } - return Ok(messages); + throw; + } } /// diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/DataRestrictionValidation.cs b/src/Altinn.App.Api/Helpers/RequestHandling/DataRestrictionValidation.cs index 82c720f4e..f7e0237dc 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/DataRestrictionValidation.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/DataRestrictionValidation.cs @@ -1,10 +1,9 @@ #nullable enable -using System.Net; using System.Net.Http.Headers; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Extensions; +using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; namespace Altinn.App.Api.Helpers.RequestHandling @@ -19,51 +18,67 @@ public static class DataRestrictionValidation /// /// the original http request /// datatype the files is beeing uploaded to - /// Null if validation passed, error response if not - /// true with errorResponse = null if all is ok, false with errorResponse if not - public static bool CompliesWithDataRestrictions(HttpRequest request, DataType? dataType, out ActionResult? errorResponse) + /// true with errorResponse = empty list if all is ok, false with errorResponse including errors if not + public static (bool Success, List Errors) CompliesWithDataRestrictions(HttpRequest request, DataType? dataType) { + List errors = new(); var errorBaseMessage = "Invalid data provided. Error:"; - errorResponse = null; if (!request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) { - errorResponse = new BadRequestObjectResult($"{errorBaseMessage} The request must include a Content-Disposition header"); - return false; + errors.Add(new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, + Severity = ValidationIssueSeverity.Error, + Description = $"{errorBaseMessage} The request must include a Content-Disposition header" + }); + + return (false, errors); } var maxSize = (long?)dataType?.MaxSize * 1024 * 1024; if (maxSize != null && request.ContentLength > maxSize) { - errorResponse = new ObjectResult($"{errorBaseMessage} Binary attachment exceeds limit of {maxSize}") + errors.Add(new ValidationIssue { - StatusCode = (int)HttpStatusCode.RequestEntityTooLarge - }; - return false; + Code = ValidationIssueCodes.DataElementCodes.DataElementTooLarge, + Severity = ValidationIssueSeverity.Error, + Description = $"{errorBaseMessage} Binary attachment exceeds limit of {maxSize}" + }); + + return (false, errors); } - ContentDispositionHeaderValue contentDisposition = ContentDispositionHeaderValue.Parse(headerValues); - string? filename = contentDisposition.FileNameStar ?? contentDisposition.FileName; + string? filename = GetFileNameFromHeader(headerValues); if (string.IsNullOrEmpty(filename)) { - errorResponse = new BadRequestObjectResult($"{errorBaseMessage} The Content-Disposition header must contain a filename"); - return false; + errors.Add(new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.MissingFileName, + Severity = ValidationIssueSeverity.Error, + Description = $"{errorBaseMessage} The Content-Disposition header must contain a filename" + }); + + return (false, errors); } - // We actively remove quotes because we don't want them replaced with '_'. - // Quotes around filename in Content-Disposition is valid, but not as part of the filename. - filename = filename.Trim('\"').AsFileName(false); string[] splitFilename = filename.Split('.'); if (splitFilename.Length < 2) { - errorResponse = new BadRequestObjectResult($"{errorBaseMessage} Invalid format for filename: {filename}. Filename is expected to end with '.{{filetype}}'."); - return false; + errors.Add(new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.InvalidFileNameFormat, + Severity = ValidationIssueSeverity.Error, + Description = $"{errorBaseMessage} Invalid format for filename: {filename}. Filename is expected to end with '.{{filetype}}'." + }); + + return (false, errors); } if (dataType?.AllowedContentTypes == null || dataType.AllowedContentTypes.Count == 0) { - return true; + return (true, errors); } string filetype = splitFilename[splitFilename.Length - 1]; @@ -71,25 +86,58 @@ public static bool CompliesWithDataRestrictions(HttpRequest request, DataType? d if (!request.Headers.TryGetValue("Content-Type", out StringValues contentType)) { - errorResponse = new BadRequestObjectResult($"{errorBaseMessage} Content-Type header must be included in request."); - return false; + errors.Add(new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.InvalidFileNameFormat, + Severity = ValidationIssueSeverity.Error, + Description = $"{errorBaseMessage} Content-Type header must be included in request." + }); + + return (false, errors); } // Verify that file mime type matches content type in request if (!contentType.Equals("application/octet-stream") && !mimeType.Equals(contentType, StringComparison.InvariantCultureIgnoreCase)) { - errorResponse = new BadRequestObjectResult($"{errorBaseMessage} Content type header {contentType} does not match mime type {mimeType} for uploaded file. Please fix header or upload another file."); - return false; + errors.Add(new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.InvalidFileNameFormat, + Severity = ValidationIssueSeverity.Error, + Description = $"{errorBaseMessage} Content type header {contentType} does not match mime type {mimeType} for uploaded file. Please fix header or upload another file." + }); + + return (false, errors); } // Verify that file mime type is an allowed content-type if (!dataType.AllowedContentTypes.Contains(mimeType, StringComparer.InvariantCultureIgnoreCase) && !dataType.AllowedContentTypes.Contains("application/octet-stream")) { - errorResponse = new BadRequestObjectResult($"{errorBaseMessage} Invalid content type: {mimeType}. Please try another file. Permitted content types include: {string.Join(", ", dataType.AllowedContentTypes)}"); - return false; + errors.Add(new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, + Severity = ValidationIssueSeverity.Error, + Description = $"{errorBaseMessage} Invalid content type: {mimeType}. Please try another file. Permitted content types include: {string.Join(", ", dataType.AllowedContentTypes)}" + }); + + return (false, errors); } - return true; + return (true, errors); + } + + /// + /// Uses the provided header to extract the filename + /// + public static string? GetFileNameFromHeader(StringValues headerValues) + { + ContentDispositionHeaderValue contentDisposition = ContentDispositionHeaderValue.Parse(headerValues); + string? filename = contentDisposition.FileNameStar ?? contentDisposition.FileName; + + // We actively remove quotes because we don't want them replaced with '_'. + // Quotes around filename in Content-Disposition is valid, but not as part of the filename. + filename = filename?.Trim('\"').AsFileName(false); + + return filename; } } } diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index b6cb0d5e5..9bba9d909 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -9,13 +9,13 @@ - + - + - - - + + + diff --git a/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs b/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs new file mode 100644 index 000000000..8ba739d8b --- /dev/null +++ b/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using System.Net.Http.Headers; + +namespace Altinn.App.Core.Extensions +{ + /// + /// Extension methods for + /// + public static class HttpContextExtensions + { + /// + /// Reads the request body and returns it as a + /// + public static StreamContent CreateContentStream(this HttpRequest request) + { + StreamContent content = new StreamContent(request.Body); + content.Headers.ContentType = MediaTypeHeaderValue.Parse(request.ContentType); + + if (request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) + { + content.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse(headerValues.ToString()); + } + + return content; + } + } +} diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index d4da8d756..80cf71a9b 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Altinn.App.Core.Features.Action; using Altinn.App.Core.Features.DataLists; using Altinn.App.Core.Features.DataProcessing; +using Altinn.App.Core.Features.FileAnalyzis; using Altinn.App.Core.Features.Options; using Altinn.App.Core.Features.PageOrder; using Altinn.App.Core.Features.Pdf; @@ -159,6 +160,8 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati AddPdfServices(services); AddEventServices(services); AddProcessServices(services); + AddFileAnalyserServices(services); + AddFileValidatorServices(services); if (!env.IsDevelopment()) { @@ -249,5 +252,17 @@ private static void AddActionServices(IServiceCollection services) services.AddHttpClient(); services.AddTransientUserActionAuthorizerForActionInAllTasks("sign"); } + + private static void AddFileAnalyserServices(IServiceCollection services) + { + services.TryAddTransient(); + services.TryAddTransient(); + } + + private static void AddFileValidatorServices(IServiceCollection services) + { + services.TryAddTransient(); + services.TryAddTransient(); + } } } diff --git a/src/Altinn.App.Core/Features/FeatureFlags.cs b/src/Altinn.App.Core/Features/FeatureFlags.cs index 54b7b4b85..38ceb70f0 100644 --- a/src/Altinn.App.Core/Features/FeatureFlags.cs +++ b/src/Altinn.App.Core/Features/FeatureFlags.cs @@ -12,4 +12,10 @@ public static class FeatureFlags /// based solution for pdf generation. /// public const string NewPdfGeneration = "NewPdfGeneration"; + + /// + /// By enabling this feature calling the POST on the /data endpoints will + /// return validation errors in the response body instead of a string. + /// + public const string JsonObjectInDataResponse = "JsonObjectInDataResponse"; } diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs new file mode 100644 index 000000000..6ed150674 --- /dev/null +++ b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs @@ -0,0 +1,29 @@ +using Altinn.App.Core.Features.FileAnalysis; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.FileAnalyzis +{ + /// + /// Factory class that resolves the correct file analysers to run on against a . + /// + public class FileAnalyserFactory : IFileAnalyserFactory + { + private readonly IEnumerable _fileAnalysers; + + /// + /// Initializes a new instance of the class. + /// + public FileAnalyserFactory(IEnumerable fileAnalysers) + { + _fileAnalysers = fileAnalysers; + } + + /// + /// Finds the specified file analyser implementations based on the specified analyser id's. + /// + public IEnumerable GetFileAnalysers(IEnumerable analyserIds) + { + return _fileAnalysers.Where(x => analyserIds.Contains(x.Id)).ToList(); + } + } +} diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs new file mode 100644 index 000000000..6bbf62d56 --- /dev/null +++ b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs @@ -0,0 +1,44 @@ +using Altinn.App.Core.Features.FileAnalyzis; + +namespace Altinn.App.Core.Features.FileAnalysis +{ + /// + /// Results from a file analysis done based the content of the file, ie. the binary data. + /// + public class FileAnalysisResult + { + /// + /// Initializes a new instance of the class. + /// + public FileAnalysisResult(string analyserId) + { + AnalyserId = analyserId; + } + + /// + /// The id of the analyser generating the result. + /// + public string AnalyserId { get; internal set; } + + /// + /// The name of the analysed file. + /// + public string? Filename { get; set; } + + /// + /// The file extension(s) without the . i.e. pdf | png | docx + /// Some mime types might have multiple extensions registered for ecample image/jpeg has both jpg and jpeg. + /// + public List Extensions { get; set; } = new List(); + + /// + /// The mime type + /// + public string? MimeType { get; set; } + + /// + /// Key/Value pairs contaning fining from the analysis. + /// + public IDictionary Metadata { get; private set; } = new Dictionary(); + } +} diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs new file mode 100644 index 000000000..84c13d0d8 --- /dev/null +++ b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs @@ -0,0 +1,44 @@ +using Altinn.App.Core.Features.FileAnalysis; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.FileAnalyzis +{ + /// + /// Analyses a file using the registred analysers on the + /// + public class FileAnalysisService : IFileAnalysisService + { + private readonly IFileAnalyserFactory _fileAnalyserFactory; + + /// + /// Initializes a new instance of the class. + /// + public FileAnalysisService(IFileAnalyserFactory fileAnalyserFactory) + { + _fileAnalyserFactory = fileAnalyserFactory; + } + + /// + /// Runs the specified file analysers against the stream provided. + /// + public async Task> Analyse(DataType dataType, Stream fileStream, string? filename = null) + { + List fileAnalysers = _fileAnalyserFactory.GetFileAnalysers(dataType.EnabledFileAnalysers).ToList(); + + List fileAnalysisResults = new(); + foreach (var analyser in fileAnalysers) + { + if (fileStream.CanSeek) + { + fileStream.Position = fileStream.Seek(0, SeekOrigin.Begin); + } + var result = await analyser.Analyse(fileStream, filename); + result.AnalyserId = analyser.Id; + result.Filename = filename; + fileAnalysisResults.Add(result); + } + + return fileAnalysisResults; + } + } +} diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs new file mode 100644 index 000000000..e430da273 --- /dev/null +++ b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs @@ -0,0 +1,20 @@ +namespace Altinn.App.Core.Features.FileAnalysis +{ + /// + /// Interface for doing extended binary file analysing. + /// + public interface IFileAnalyser + { + /// + /// The id of the analyser to be used when enabling it from config. + /// + public string Id { get; } + + /// + /// Analyses a stream with the intent to extract metadata. + /// + /// The stream to analyse. One stream = one file. + /// Filename. Optional parameter if the implementation needs the name of the file, relative or absolute path. + public Task Analyse(Stream stream, string? filename = null); + } +} diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs new file mode 100644 index 000000000..31a5f37eb --- /dev/null +++ b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs @@ -0,0 +1,16 @@ +using Altinn.App.Core.Features.FileAnalysis; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.FileAnalyzis +{ + /// + /// Interface responsible for resolving the correct file analysers to run on against a . + /// + public interface IFileAnalyserFactory + { + /// + /// Finds analyser implementations based on the specified id's provided. + /// + IEnumerable GetFileAnalysers(IEnumerable analyserIds); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs new file mode 100644 index 000000000..8ee841372 --- /dev/null +++ b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs @@ -0,0 +1,19 @@ +using Altinn.App.Core.Features.FileAnalysis; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.FileAnalyzis +{ + /// + /// Interface for running all file analysers registered on a data type. + /// + public interface IFileAnalysisService + { + /// + /// Analyses the the specified file stream. + /// + /// The where the anlysers are registered. + /// The file stream to analyse + /// The name of the file + Task> Analyse(DataType dataType, Stream fileStream, string? filename = null); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/FileValidationService.cs b/src/Altinn.App.Core/Features/Validation/FileValidationService.cs new file mode 100644 index 000000000..7ee943dce --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/FileValidationService.cs @@ -0,0 +1,44 @@ +using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation +{ + /// + /// Validates files according to the registered IFileValidation interfaces + /// + public class FileValidationService : IFileValidationService + { + private readonly IFileValidatorFactory _fileValidatorFactory; + + /// + /// Initializes a new instance of the class. + /// + public FileValidationService(IFileValidatorFactory fileValidatorFactory) + { + _fileValidatorFactory = fileValidatorFactory; + } + + /// + /// Runs all registered validators on the specified + /// + public async Task<(bool Success, List Errors)> Validate(DataType dataType, IEnumerable fileAnalysisResults) + { + List allErrors = new(); + bool allSuccess = true; + + List fileValidators = _fileValidatorFactory.GetFileValidators(dataType.EnabledFileValidators).ToList(); + foreach (IFileValidator fileValidator in fileValidators) + { + (bool success, IEnumerable errors) = await fileValidator.Validate(dataType, fileAnalysisResults); + if (!success) + { + allSuccess = false; + allErrors.AddRange(errors); + } + } + + return (allSuccess, allErrors); + } + } +} diff --git a/src/Altinn.App.Core/Features/Validation/FileValidatorFactory.cs b/src/Altinn.App.Core/Features/Validation/FileValidatorFactory.cs new file mode 100644 index 000000000..aa607127d --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/FileValidatorFactory.cs @@ -0,0 +1,28 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation +{ + /// + /// Factory class that resolves the correct file validators to run on against a . + /// + public class FileValidatorFactory : IFileValidatorFactory + { + private readonly IEnumerable _fileValidators; + + /// + /// Initializes a new instance of the class. + /// + public FileValidatorFactory(IEnumerable fileValidators) + { + _fileValidators = fileValidators; + } + + /// + /// Finds the specified file analyser implementations based on the specified analyser id's. + /// + public IEnumerable GetFileValidators(IEnumerable validatorIds) + { + return _fileValidators.Where(x => validatorIds.Contains(x.Id)).ToList(); + } + } +} diff --git a/src/Altinn.App.Core/Features/Validation/IFileValidationService.cs b/src/Altinn.App.Core/Features/Validation/IFileValidationService.cs new file mode 100644 index 000000000..c90f0ee25 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/IFileValidationService.cs @@ -0,0 +1,17 @@ +using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation +{ + /// + /// Interface for running all file validators registered on a data type. + /// + public interface IFileValidationService + { + /// + /// Validates the file based on the file analysis results. + /// + Task<(bool Success, List Errors)> Validate(DataType dataType, IEnumerable fileAnalysisResults); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/IFileValidator.cs b/src/Altinn.App.Core/Features/Validation/IFileValidator.cs new file mode 100644 index 000000000..2fee12df6 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/IFileValidator.cs @@ -0,0 +1,22 @@ +using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation +{ + /// + /// Interface for handling validation of files added to an instance. + /// + public interface IFileValidator + { + /// + /// The id of the validator to be used when enabling it from config. + /// + public string Id { get; } + + /// + /// Validating a file based on analysis results. + /// + Task<(bool Success, IEnumerable Errors)> Validate(DataType dataType, IEnumerable fileAnalysisResults); + } +} diff --git a/src/Altinn.App.Core/Features/Validation/IFileValidatorFactory.cs b/src/Altinn.App.Core/Features/Validation/IFileValidatorFactory.cs new file mode 100644 index 000000000..2c1f18c8c --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/IFileValidatorFactory.cs @@ -0,0 +1,15 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation +{ + /// + /// Interface responsible for resolving the correct file validators to run on against a . + /// + public interface IFileValidatorFactory + { + /// + /// Finds validator implementations based on the specified id's provided. + /// + IEnumerable GetFileValidators(IEnumerable validatorIds); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs b/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs index 13302216e..13d4baf21 100644 --- a/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs +++ b/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using System.Text.Json.Serialization.Metadata; namespace Altinn.App.Core.Helpers @@ -29,12 +28,12 @@ public void ModifyPrefixInfo(JsonTypeInfo ti) } /// - /// This class extends the IList interface with a RemoveAll method. + /// This class extends the ]]> interface with a RemoveAll method. /// public static class ListHelpers { /// - /// IList implementation of List.RemoveAll method. + /// ]]> implementation of .RemoveAll]]> method. /// public static void RemoveAll(this IList list, Predicate predicate) { diff --git a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs index 924f71915..4d5a33dcc 100644 --- a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs +++ b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs @@ -110,7 +110,17 @@ public Application GetApplication() { try { - return _appMetadata.GetApplicationMetadata().Result; + ApplicationMetadata applicationMetadata = _appMetadata.GetApplicationMetadata().Result; + Application application = applicationMetadata; + if (applicationMetadata.OnEntry != null) + { + application.OnEntry = new OnEntryConfig() + { + Show = applicationMetadata.OnEntry.Show + }; + } + + return application; } catch (AggregateException ex) { diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsSubscriptionClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsSubscriptionClient.cs index 6b0b71022..7ade4288a 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsSubscriptionClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Events/EventsSubscriptionClient.cs @@ -57,7 +57,7 @@ public async Task AddSubscription(string org, string app, string e { TypeFilter = eventType, EndPoint = new Uri($"{appBaseUrl}api/v1/eventsreceiver?code={await _secretCodeProvider.GetSecretCode()}"), - SourceFilter = new Uri(appBaseUrl) + SourceFilter = new Uri(appBaseUrl.TrimEnd('/')) // The event system is requireing the source filter to be without trailing slash }; string serializedSubscriptionRequest = JsonSerializer.Serialize(subscriptionRequest); diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index 7d4515258..c9fcc48e6 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -276,7 +276,7 @@ public async Task InsertBinaryData(string org, string app, int inst string token = _userTokenProvider.GetUserToken(); DataElement dataElement; - StreamContent content = CreateContentStream(request); + StreamContent content = request.CreateContentStream(); HttpResponseMessage response = await _client.PostAsync(token, apiUrl, content); @@ -335,7 +335,7 @@ public async Task UpdateBinaryData(string org, string app, int inst string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}"; string token = _userTokenProvider.GetUserToken(); - StreamContent content = CreateContentStream(request); + StreamContent content = request.CreateContentStream(); HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content); @@ -375,19 +375,6 @@ public async Task UpdateBinaryData(InstanceIdentifier instanceIdent throw await PlatformHttpException.CreateAsync(response); } - private static StreamContent CreateContentStream(HttpRequest request) - { - StreamContent content = new StreamContent(request.Body); - content.Headers.ContentType = MediaTypeHeaderValue.Parse(request.ContentType); - - if (request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) - { - content.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse(headerValues.ToString()); - } - - return content; - } - /// public async Task Update(Instance instance, DataElement dataElement) { diff --git a/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs b/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs index f6d3826f9..2fb071f33 100644 --- a/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs +++ b/src/Altinn.App.Core/Internal/App/FrontendFeatures.cs @@ -1,3 +1,6 @@ +using Altinn.App.Core.Features; +using Microsoft.FeatureManagement; + namespace Altinn.App.Core.Internal.App { /// @@ -10,10 +13,19 @@ public class FrontendFeatures : IFrontendFeatures /// /// Default implementation of IFrontendFeatures /// - public FrontendFeatures() + public FrontendFeatures(IFeatureManager featureManager) { features.Add("footer", true); features.Add("processActions", true); + + if (featureManager.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse).Result) + { + features.Add("jsonObjectInDataResponse", true); + } + else + { + features.Add("jsonObjectInDataResponse", false); + } } /// diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs index 7434b1a6b..ee498c06a 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs @@ -1,5 +1,4 @@ using System.Globalization; -using System.Text.Json; using System.Text.RegularExpressions; using Altinn.App.Core.Models.Expressions; @@ -76,6 +75,15 @@ public static bool EvaluateBooleanExpression(LayoutEvaluatorState state, Compone ExpressionFunction.and => And(args), ExpressionFunction.or => Or(args), ExpressionFunction.not => Not(args), + ExpressionFunction.contains => Contains(args), + ExpressionFunction.notContains => !Contains(args), + ExpressionFunction.commaContains => CommaContains(args), + ExpressionFunction.endsWith => EndsWith(args), + ExpressionFunction.startsWith => StartsWith(args), + ExpressionFunction.stringLength => StringLength(args), + ExpressionFunction.round => Round(args), + ExpressionFunction.upperCase => UpperCase(args), + ExpressionFunction.lowerCase => LowerCase(args), ExpressionFunction.gatewayAction => state.GetGatewayAction(), _ => throw new ExpressionEvaluatorTypeErrorException($"Function \"{expr.Function}\" not implemented"), }; @@ -120,6 +128,127 @@ public static bool EvaluateBooleanExpression(LayoutEvaluatorState state, Compone return string.Join("", args.Select(a => a switch { string s => s, _ => ToStringForEquals(a) })); } + private static bool Contains(object?[] args) + { + if (args.Length != 2) + { + throw new ExpressionEvaluatorTypeErrorException($"Expected 2 argument(s), got {args.Length}"); + } + string? stringOne = ToStringForEquals(args[0]); + string? stringTwo = ToStringForEquals(args[1]); + + if (stringOne is null || stringTwo is null) + { + return false; + } + + return stringOne.Contains(stringTwo, StringComparison.InvariantCulture); + } + + private static bool EndsWith(object?[] args) + { + if (args.Length != 2) + { + throw new ExpressionEvaluatorTypeErrorException($"Expected 2 argument(s), got {args.Length}"); + } + string? stringOne = ToStringForEquals(args[0]); + string? stringTwo = ToStringForEquals(args[1]); + + if (stringOne is null || stringTwo is null) + { + return false; + } + + return stringOne.EndsWith(stringTwo, StringComparison.InvariantCulture); + } + + private static bool StartsWith(object?[] args) + { + if (args.Length != 2) + { + throw new ExpressionEvaluatorTypeErrorException($"Expected 2 argument(s), got {args.Length}"); + } + string? stringOne = ToStringForEquals(args[0]); + string? stringTwo = ToStringForEquals(args[1]); + + if (stringOne is null || stringTwo is null) + { + return false; + } + + return stringOne.StartsWith(stringTwo, StringComparison.InvariantCulture); + } + + private static bool CommaContains(object?[] args) + { + if (args.Length != 2) + { + throw new ExpressionEvaluatorTypeErrorException($"Expected 2 arguments, got {args.Length}"); + } + string? stringOne = ToStringForEquals(args[0]); + string? stringTwo = ToStringForEquals(args[1]); + + if (stringOne is null || stringTwo is null) + { + return false; + } + + return stringOne.Split(",").Select(s => s.Trim()).Contains(stringTwo, StringComparer.InvariantCulture); + } + + private static int StringLength(object?[] args) + { + if (args.Length != 1) + { + throw new ExpressionEvaluatorTypeErrorException($"Expected 1 argument, got {args.Length}"); + } + string? stringOne = ToStringForEquals(args[0]); + return stringOne?.Length ?? 0; + } + + private static string Round(object?[] args) + { + if (args.Length < 1 || args.Length> 2) + { + throw new ExpressionEvaluatorTypeErrorException($"Expected 1-2 argument(s), got {args.Length}"); + } + + var number = PrepareNumericArg(args[0]); + + if(number is null) + { + number = 0; + } + + int precision = 0; + if (args.Length == 2 && args[1] is not null) + { + precision = Convert.ToInt32(args[1]); + } + + return number.Value.ToString($"N{precision}", CultureInfo.InvariantCulture); + } + + private static string? UpperCase(object?[] args) + { + if (args.Length != 1) + { + throw new ExpressionEvaluatorTypeErrorException($"Expected 1 argument, got {args.Length}"); + } + string? stringOne = ToStringForEquals(args[0]); + return stringOne?.ToUpperInvariant(); + } + + private static string? LowerCase(object?[] args) + { + if (args.Length != 1) + { + throw new ExpressionEvaluatorTypeErrorException($"Expected 1 argument, got {args.Length}"); + } + string? stringOne = ToStringForEquals(args[0]); + return stringOne?.ToLowerInvariant(); + } + private static bool PrepareBooleanArg(object? arg) { return arg switch @@ -188,27 +317,25 @@ private static (double?, double?) PrepareNumericArgs(object?[] args) { throw new ExpressionEvaluatorTypeErrorException("Invalid number of args for compare"); } - var a = args[0] switch - { - bool ab => throw new ExpressionEvaluatorTypeErrorException($"Expected number, got value {(ab ? "true" : "false")}"), - string s => parseNumber(s), - decimal d => (double)d, - int i => (double)i, - object o => o as double?, // assume all relevant numers are representable as double (as in frontend) - _ => null, - }; - var b = args[1] switch + var a = PrepareNumericArg(args[0]); + + var b = PrepareNumericArg(args[1]); + + return (a, b); + } + + private static double? PrepareNumericArg(object? arg) + { + return arg switch { - bool bb => throw new ExpressionEvaluatorTypeErrorException($"Expected number, got value {(bb ? "true" : "false")}"), + bool ab => throw new ExpressionEvaluatorTypeErrorException($"Expected number, got value {(ab ? "true" : "false")}"), string s => parseNumber(s), decimal d => (double)d, int i => (double)i, - object o => o as double?, // assume all relevant numers are representable as double (as in frontend) - _ => null, + object o => o as double?, // assume all relevant numbers are representable as double (as in frontend) + _ => null }; - - return (a, b); } private static object? IfImpl(object?[] args) diff --git a/src/Altinn.App.Core/Models/AppOptions.cs b/src/Altinn.App.Core/Models/AppOptions.cs index d4d95f3f2..d0dac984d 100644 --- a/src/Altinn.App.Core/Models/AppOptions.cs +++ b/src/Altinn.App.Core/Models/AppOptions.cs @@ -1,3 +1,5 @@ +using System.Reflection.Metadata.Ecma335; + namespace Altinn.App.Core.Models { /// @@ -8,7 +10,14 @@ public class AppOptions /// /// Gets or sets the list of options. /// - public List Options { get; set; } + public List Options { get; set; } = new List(); + + /// + /// Gets or sets the parameters used to generate the options. + /// The dictionary key is the name of the parameter and the value is the value of the parameter. + /// This can be used to document the parameters used to generate the options. + /// + public Dictionary Parameters{ get; set; } = new Dictionary(); /// /// Gets or sets a value indicating whether the options can be cached. diff --git a/src/Altinn.App.Core/Models/ApplicationMetadata.cs b/src/Altinn.App.Core/Models/ApplicationMetadata.cs index f3d5fa79a..231a9d2a5 100644 --- a/src/Altinn.App.Core/Models/ApplicationMetadata.cs +++ b/src/Altinn.App.Core/Models/ApplicationMetadata.cs @@ -39,6 +39,12 @@ public ApplicationMetadata(string id) [JsonProperty(PropertyName = "features")] public Dictionary? Features { get; set; } + /// + /// Configure options for handling what happens when entering the application + /// + [JsonProperty(PropertyName = "onEntry")] + public new OnEntry? OnEntry { get; set; } + /// /// Get AppIdentifier based on ApplicationMetadata.Id /// Updated by setting ApplicationMetadata.Id diff --git a/src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs b/src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs index 3d91e4f6a..41ffb6f41 100644 --- a/src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs +++ b/src/Altinn.App.Core/Models/Expressions/ExpressionFunctionEnum.cs @@ -34,6 +34,34 @@ public enum ExpressionFunction /// concat, /// + /// Turn characters to upper case + /// + upperCase, + /// + /// Turn characters to lower case + /// + lowerCase, + /// + /// Check if a string contains another string + /// + contains, + /// + /// Check if a string does not contain another string + /// + notContains, + /// + /// Check if a comma separated string contains a value + /// + commaContains, + /// + /// Check if a string ends with another string + /// + endsWith, + /// + /// Check if a string starts with another string + /// + startsWith, + /// /// Check if values are equal /// equals, @@ -58,6 +86,14 @@ public enum ExpressionFunction /// greaterThan, /// + /// Return the length of a string + /// + stringLength, + /// + /// Rounds a number to an integer, or optionally a decimal with a configurable amount of decimal points + /// + round, + /// /// Return true if all the expressions evaluate to true /// and, diff --git a/src/Altinn.App.Core/Models/InstanceSelection.cs b/src/Altinn.App.Core/Models/InstanceSelection.cs new file mode 100644 index 000000000..44a294c6f --- /dev/null +++ b/src/Altinn.App.Core/Models/InstanceSelection.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; + +namespace Altinn.App.Core.Models +{ + /// + /// Contains options for displaying the instance selection component + /// + public class InstanceSelection + { + private int? _defaultSelectedOption; + + /// + /// A list of selectable options for amount of rows per page to show for pagination + /// + [JsonProperty(PropertyName = "rowsPerPageOptions")] + public List? RowsPerPageOptions { get; set; } + + /// + /// The default amount of rows per page to show for pagination + /// + [JsonProperty(PropertyName = "defaultRowsPerPage")] + public int? DefaultRowsPerPage { get; set; } + + /// + /// The default selected option for rows per page to show for pagination + /// + [JsonProperty(PropertyName = "defaultSelectedOption")] + public int? DefaultSelectedOption + { + get { return _defaultSelectedOption ?? DefaultRowsPerPage; } + set { _defaultSelectedOption = value; } + } + + /// + /// The direction of sorting the list of instances, asc or desc + /// + [JsonProperty(PropertyName = "sortDirection")] + public string? SortDirection { get; set; } + } +} diff --git a/src/Altinn.App.Core/Models/OnEntry.cs b/src/Altinn.App.Core/Models/OnEntry.cs new file mode 100644 index 000000000..e22186ee5 --- /dev/null +++ b/src/Altinn.App.Core/Models/OnEntry.cs @@ -0,0 +1,17 @@ +using Altinn.Platform.Storage.Interface.Models; +using Newtonsoft.Json; + +namespace Altinn.App.Core.Models { + + /// + /// The on entry configuration + /// + public class OnEntry : OnEntryConfig { + + /// + /// Options for displaying the instance selection component + /// + [JsonProperty(PropertyName = "instanceSelection")] + public InstanceSelection? InstanceSelection { get; set; } + } +} diff --git a/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs b/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs index d45cb9699..c10b85366 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs @@ -19,30 +19,42 @@ public class ValidationIssue /// The unique id of the specific element with the identified issue. /// [JsonProperty(PropertyName = "instanceId")] - public string InstanceId { get; set; } + public string? InstanceId { get; set; } /// /// The uniqe id of the data element of a given instance with the identified issue. /// [JsonProperty(PropertyName = "dataElementId")] - public string DataElementId { get; set; } + public string? DataElementId { get; set; } /// /// A reference to a property the issue is a bout. /// [JsonProperty(PropertyName = "field")] - public string Field { get; set; } + public string? Field { get; set; } /// /// A system readable identification of the type of issue. /// [JsonProperty(PropertyName = "code")] - public string Code { get; set; } + public string? Code { get; set; } /// /// A human readable description of the issue. /// [JsonProperty(PropertyName = "description")] - public string Description { get; set; } + public string? Description { get; set; } + + /// + /// The validation source of the issue eg. File, Schema, Component + /// + [JsonProperty(PropertyName = "source")] + public string? Source { get; set; } + + /// + /// The custom text key to use for the localized text in the frontend. + /// + [JsonProperty(PropertyName = "customTextKey")] + public string? CustomTextKey { get; set; } } } diff --git a/src/Altinn.App.Core/Models/Validation/ValidationIssueCodes.cs b/src/Altinn.App.Core/Models/Validation/ValidationIssueCodes.cs index f4ca9967a..9966acb67 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationIssueCodes.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationIssueCodes.cs @@ -55,6 +55,16 @@ public static class DataElementCodes /// Gets a value that represents a validation issue where the data element is infected with virus or malware of some form. /// public static string DataElementFileInfected => nameof(DataElementFileInfected); + + /// + /// Gets a value that represents a validation issue where the data element has a file name that is not allowed. + /// + public static string InvalidFileNameFormat => nameof(InvalidFileNameFormat); + + /// + /// Gets a value that represents a validation issue where the data element is missing a file name. + /// + public static string MissingFileName => nameof(MissingFileName); } } } diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj index 7ced28260..20718c649 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -4,22 +4,21 @@ net6.0 enable enable - false - - - + + + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs new file mode 100644 index 000000000..d341e654a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs @@ -0,0 +1,178 @@ +using Altinn.App.Api.Tests.Utils; +using Microsoft.AspNetCore.Mvc.Testing; +using System.Net.Http.Headers; +using System.Net; +using Xunit; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Core.Features.FileAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Api.Tests.Controllers +{ + public class DataControllerTests : ApiTestBase, IClassFixture> + { + public DataControllerTests(WebApplicationFactory factory) : base(factory) + { + } + + [Fact] + public async Task CreateDataElement_BinaryPdf_AnalyserShouldRunOk() + { + OverrideServicesForThisTest = (services) => + { + services.AddTransient(); + services.AddTransient(); + }; + + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + + Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + + // Setup the request + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + ByteArrayContent fileContent = await CreateBinaryContent(org, app, "example.pdf", "application/pdf"); + string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; + var request = new HttpRequestMessage(HttpMethod.Post, url) + { + Content = fileContent + }; + + // This is where it happens + HttpResponseMessage response = await client.SendAsync(request); + + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } + + [Fact] + public async Task CreateDataElement_JpgFakedAsPdf_AnalyserShouldRunAndFail() + { + OverrideServicesForThisTest = (services) => + { + services.AddTransient(); + services.AddTransient(); + }; + + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + + Guid guid = new Guid("1fc98a23-fe31-4ef5-8fb9-dd3f479354ce"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + + // Setup the request + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + ByteArrayContent fileContent = await CreateBinaryContent(org, app, "example.jpg.pdf", "application/pdf"); + string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; + var request = new HttpRequestMessage(HttpMethod.Post, url) + { + Content = fileContent + }; + + // This is where it happens + HttpResponseMessage response = await client.SendAsync(request); + string responseContent = await response.Content.ReadAsStringAsync(); + + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + private static async Task CreateBinaryContent(string org, string app, string filename, string mediaType) + { + var pdfFilePath = TestData.GetAppSpecificTestdataFile(org, app, filename); + var fileBytes = await File.ReadAllBytesAsync(pdfFilePath); + var fileContent = new ByteArrayContent(fileBytes); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(mediaType); + fileContent.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse($"attachment; filename=\"{filename}\"; filename*=UTF-8''{filename}"); + return fileContent; + } + } + + public class MimeTypeAnalyserSuccessStub : IFileAnalyser + { + public string Id { get; private set; } = "mimeTypeAnalyser"; + public Task> Analyse(IEnumerable httpContents) + { + throw new NotImplementedException(); + } + + public Task Analyse(Stream stream, string? filename = null) + { + return Task.FromResult(new FileAnalysisResult(Id) + { + + MimeType = "application/pdf", + Filename = "example.pdf", + Extensions = new List() { "pdf" } + }); + } + } + + public class MimeTypeAnalyserFailureStub : IFileAnalyser + { + public string Id { get; private set; } = "mimeTypeAnalyser"; + public Task> Analyse(IEnumerable httpContents) + { + throw new NotImplementedException(); + } + + public Task Analyse(Stream stream, string? filename = null) + { + return Task.FromResult(new FileAnalysisResult(Id) + { + + MimeType = "application/jpeg", + Filename = "example.jpg.pdf", + Extensions = new List() { "jpg" } + }); + } + } + + public class MimeTypeValidatorStub : IFileValidator + { + public string Id { get; private set; } = "mimeTypeValidator"; + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously. Suppressed because of the interface. + public async Task<(bool Success, IEnumerable Errors)> Validate(DataType dataType, IEnumerable fileAnalysisResults) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + List errors = new(); + + var fileMimeTypeResult = fileAnalysisResults.FirstOrDefault(x => x.MimeType != null); + + // Verify that file mime type is an allowed content-type + if (!dataType.AllowedContentTypes.Contains(fileMimeTypeResult?.MimeType, StringComparer.InvariantCultureIgnoreCase) && !dataType.AllowedContentTypes.Contains("application/octet-stream")) + { + ValidationIssue error = new() + { + Source = "File", + Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, + Severity = ValidationIssueSeverity.Error, + Description = $"The {fileMimeTypeResult?.Filename + " "}file does not appear to be of the allowed content type according to the configuration for data type {dataType.Id}. Allowed content types are {string.Join(", ", dataType.AllowedContentTypes)}" + }; + + errors.Add(error); + + return (false, errors); + } + + return (true, errors); + } + } +} diff --git a/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs index 0092901cd..51e522b0a 100644 --- a/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/FileScanControllerTests.cs @@ -1,5 +1,4 @@ using Altinn.App.Api.Controllers; -using Altinn.App.Core.Infrastructure.Clients; using Altinn.App.Core.Internal.Instances; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs index b29cb84b8..09bf34d14 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs @@ -1,6 +1,7 @@ +using System.Net; using Altinn.App.Api.Controllers; using Altinn.App.Core.Features.Validation; -using Altinn.App.Core.Infrastructure.Clients; +using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models.Validation; @@ -153,4 +154,92 @@ public async Task ValidateInstance_returns_OK_with_messages() // Assert result.Should().BeOfType().Which.Value.Should().BeEquivalentTo(validationResult); } + + [Fact] + public async Task ValidateInstance_returns_403_when_not_authorized() + { + // Arrange + var instanceMock = new Mock(); + var appMetadataMock = new Mock(); + var validationMock = new Mock(); + + const string org = "ttd"; + const string app = "app"; + const int instanceOwnerPartyId = 1337; + var instanceId = Guid.NewGuid(); + + Instance instance = new Instance + { + Id = "instanceId", + Process = new ProcessState + { + CurrentTask = new ProcessElementInfo + { + ElementId = "dummy" + } + } + }; + + var updateProcessResult = new HttpResponseMessage(HttpStatusCode.Forbidden); + PlatformHttpException exception = await PlatformHttpException.CreateAsync(updateProcessResult); + + instanceMock.Setup(i => i.GetInstance(app, org, instanceOwnerPartyId, instanceId)) + .Returns(Task.FromResult(instance)); + + validationMock.Setup(v => v.ValidateAndUpdateProcess(instance, "dummy")) + .Throws(exception); + + // Act + var validateController = + new ValidateController(instanceMock.Object, validationMock.Object, appMetadataMock.Object); + var result = await validateController.ValidateInstance(org, app, instanceOwnerPartyId, instanceId); + + // Assert + result.Should().BeOfType().Which.StatusCode.Should().Be(403); + } + + [Fact] + public async Task ValidateInstance_throws_PlatformHttpException_when_not_403() + { + // Arrange + var instanceMock = new Mock(); + var appMetadataMock = new Mock(); + var validationMock = new Mock(); + + const string org = "ttd"; + const string app = "app"; + const int instanceOwnerPartyId = 1337; + var instanceId = Guid.NewGuid(); + + Instance instance = new Instance + { + Id = "instanceId", + Process = new ProcessState + { + CurrentTask = new ProcessElementInfo + { + ElementId = "dummy" + } + } + }; + + var updateProcessResult = new HttpResponseMessage(HttpStatusCode.BadRequest); + PlatformHttpException exception = await PlatformHttpException.CreateAsync(updateProcessResult); + + instanceMock.Setup(i => i.GetInstance(app, org, instanceOwnerPartyId, instanceId)) + .Returns(Task.FromResult(instance)); + + validationMock.Setup(v => v.ValidateAndUpdateProcess(instance, "dummy")) + .Throws(exception); + + // Act + var validateController = + new ValidateController(instanceMock.Object, validationMock.Object, appMetadataMock.Object); + + // Assert + var thrownException = await Assert.ThrowsAsync(() => + validateController.ValidateInstance(org, app, instanceOwnerPartyId, instanceId)); + Assert.Equal(exception, thrownException); + } + } diff --git a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs new file mode 100644 index 000000000..239ec84fd --- /dev/null +++ b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs @@ -0,0 +1,54 @@ +using Altinn.App.Api.Tests.Data; +using Altinn.App.Core.Configuration; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Altinn.App.Api.Tests; + +public class ApiTestBase +{ + private readonly WebApplicationFactory _factory; + + public ApiTestBase(WebApplicationFactory factory) + { + _factory = factory; + } + + /// + /// Gets a client that adds appsettings from the specified org/app + /// test application under TestData/Apps to the service collection. + /// + public HttpClient GetRootedClient(string org, string app) + { + string appRootPath = TestData.GetApplicationDirectory(org, app); + string appSettingsPath = Path.Join(appRootPath, "appsettings.json"); + + var client = _factory.WithWebHostBuilder(builder => + { + var configuration = new ConfigurationBuilder() + .AddJsonFile(appSettingsPath) + .Build(); + + configuration.GetSection("AppSettings:AppBasePath").Value = appRootPath; + IConfigurationSection appSettingSection = configuration.GetSection("AppSettings"); + + builder.ConfigureServices(services => services.Configure(appSettingSection)); + builder.ConfigureTestServices(services => OverrideServicesForAllTests(services)); + builder.ConfigureTestServices(OverrideServicesForThisTest); + }).CreateClient(); + + return client; + } + + /// + /// Set this in your test class constructor to make the same overrides for all tests. + /// + public Action OverrideServicesForAllTests { get; set; } = (services) => { }; + + /// + /// Set this within a test to override the service just for that test. + /// + public Action OverrideServicesForThisTest { get; set; } = (services) => { }; +} diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/1337/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/1337/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd.pretest.json new file mode 100644 index 000000000..c2ee5fe03 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/1337/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd.pretest.json @@ -0,0 +1,39 @@ +{ + "id": "1337/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd", + "instanceOwner": { + "partyId": "1337", + "personNumber": "01039012345" + }, + "appId": "tdd/contributer-restriction", + "org": "tdd", + "process": { + "started": "2019-12-05T13:24:34.8412179Z", + "startEvent": "StartEvent_1", + "currentTask": { + "flow": 2, + "started": "2019-12-05T13:24:34.9196661Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "validated": { + "timestamp": "2020-02-07T10:46:36.985894+01:00", + "canCompleteTask": false + } + } + }, + "status": { + "isArchived": false, + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + }, + "data": [ + { + "id": "de288942-a8af-4f77-a1f1-6e1ede1cd502", + "dataType": "default", + "contentType": "application/xml", + "size": 0, + "locked": false + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/1337/1fc98a23-fe31-4ef5-8fb9-dd3f479354ce.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/1337/1fc98a23-fe31-4ef5-8fb9-dd3f479354ce.pretest.json new file mode 100644 index 000000000..c2ee5fe03 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/1337/1fc98a23-fe31-4ef5-8fb9-dd3f479354ce.pretest.json @@ -0,0 +1,39 @@ +{ + "id": "1337/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd", + "instanceOwner": { + "partyId": "1337", + "personNumber": "01039012345" + }, + "appId": "tdd/contributer-restriction", + "org": "tdd", + "process": { + "started": "2019-12-05T13:24:34.8412179Z", + "startEvent": "StartEvent_1", + "currentTask": { + "flow": 2, + "started": "2019-12-05T13:24:34.9196661Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "validated": { + "timestamp": "2020-02-07T10:46:36.985894+01:00", + "canCompleteTask": false + } + } + }, + "status": { + "isArchived": false, + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + }, + "data": [ + { + "id": "de288942-a8af-4f77-a1f1-6e1ede1cd502", + "dataType": "default", + "contentType": "application/xml", + "size": 0, + "locked": false + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/TestData.cs b/test/Altinn.App.Api.Tests/Data/TestData.cs index dde77dc9e..e67c0cd33 100644 --- a/test/Altinn.App.Api.Tests/Data/TestData.cs +++ b/test/Altinn.App.Api.Tests/Data/TestData.cs @@ -1,35 +1,147 @@ using Altinn.App.Api.Tests.Mocks; -using System; -using System.IO; +using System.Linq; -namespace Altinn.App.Api.Tests.Data +namespace Altinn.App.Api.Tests.Data; + +public static class TestData { - public static class TestData + public static string GetTestDataRootDirectory() { - public static string GetTestDataRootFolder() - { - var assemblyPath = new Uri(typeof(TestData).Assembly.Location).LocalPath; + var assemblyPath = new Uri(typeof(TestData).Assembly.Location).LocalPath; + var assemblyFolder = Path.GetDirectoryName(assemblyPath); - return Path.Combine(assemblyPath, @".././../Data"); - } + return Path.Combine(assemblyFolder!, @"../../../Data/"); + } + + public static string GetApplicationDirectory(string org, string app) + { + string testDataDirectory = GetTestDataRootDirectory(); + return Path.Combine(testDataDirectory, "apps", org, app); + } + + public static string GetAppSpecificTestdataDirectory(string org, string app) + { + var appDirectory = GetApplicationDirectory(org, app); + return Path.Join(appDirectory, "_testdata_"); + } + + public static string GetAppSpecificTestdataFile(string org, string app, string fileName) + { + var appSpecifictTestdataDirectory = GetAppSpecificTestdataDirectory(org, app); + return Path.Join(appSpecifictTestdataDirectory, fileName); + } + + public static string GetApplicationMetadataPath(string org, string app) + { + string applicationMetadataPath = GetApplicationDirectory(org, app); + return Path.Combine(applicationMetadataPath, "config", "applicationmetadata.json"); + } + + public static string GetInstancesDirectory() + { + string? testDataDirectory = GetTestDataRootDirectory(); + return Path.Combine(testDataDirectory!, @"Instances"); + } + + public static string GetDataDirectory(string org, string app, int instanceOwnerId, Guid instanceGuid) + { + string instancesDirectory = GetInstancesDirectory(); + return Path.Combine(instancesDirectory, org, app, instanceOwnerId.ToString(), instanceGuid.ToString()) + Path.DirectorySeparatorChar; + } + + public static string GetDataElementPath(string org, string app, int instanceOwnerId, Guid instanceGuid, Guid dataGuid) + { + string dataDirectory = GetDataDirectory(org, app, instanceOwnerId, instanceGuid); + return Path.Combine(dataDirectory, $"{dataGuid}.json"); + } + + public static string GetDataBlobPath(string org, string app, int instanceOwnerId, Guid instanceGuid, Guid dataGuid) + { + string dataDirectory = GetDataDirectory(org, app, instanceOwnerId, instanceGuid); + return Path.Combine(dataDirectory, "blob", dataGuid.ToString()); + } + + public static string GetTestDataRolesFolder(int userId, int resourcePartyId) + { + string testDataDirectory = GetTestDataRootDirectory(); + return Path.Combine(testDataDirectory, @"authorization/Roles/User_" + userId, "party_" + resourcePartyId, "roles.json"); + } + + public static string GetAltinnAppsPolicyPath(string org, string app) + { + string testDataDirectory = GetTestDataRootDirectory(); + return Path.Combine(testDataDirectory, "apps", org, app, "config", "authorization") + Path.DirectorySeparatorChar; + } - public static string GetTestDataInstancesFolder() + public static void DeleteInstance(string org, string app, int instanceOwnerId, Guid instanceGuid) + { + string instancePath = GetInstancePath(org, app, instanceOwnerId, instanceGuid); + if (File.Exists(instancePath)) { - string? testDataFolder = Path.GetDirectoryName(TestData.GetTestDataRootFolder()); + File.Delete(instancePath); + } + } + + public static string GetInstancePath(string org, string app, int instanceOwnerId, Guid instanceGuid) + { + string instancesDirectory = GetInstancesDirectory(); + return Path.Combine(instancesDirectory, org, app, instanceOwnerId.ToString(), instanceGuid + @".json"); + } - return Path.Combine(testDataFolder!, @"Instances"); + public static void PrepareInstance(string org, string app, int instanceOwnerId, Guid instanceGuid) + { + string instancePath = GetInstancePath(org, app, instanceOwnerId, instanceGuid); + + string preInstancePath = instancePath.Replace(".json", ".pretest.json"); + + File.Copy(preInstancePath, instancePath, true); + + string dataPath = GetDataDirectory(org, app, instanceOwnerId, instanceGuid); + + if (Directory.Exists(dataPath)) + { + foreach (string filePath in Directory.GetFiles(dataPath, "*.*", SearchOption.AllDirectories)) + { + if (filePath.Contains(".pretest.json")) + { + // Handling all data elements + File.Copy(filePath, filePath.Replace(".pretest.json", ".json"), true); + } + else if (filePath.EndsWith(".pretest")) + { + // Handling all data blobs + File.Copy(filePath, filePath.Replace(".pretest", string.Empty), true); + } + } } + } - public static string GetTestDataRolesFolder(int userId, int resourcePartyId) + public static void DeleteInstanceAndData(string org, string app, int instanceOwnerId, Guid instanceGuid) + { + DeleteDataForInstance(org, app, instanceOwnerId, instanceGuid); + + string instancePath = GetInstancePath(org, app, instanceOwnerId, instanceGuid); + if (File.Exists(instancePath)) { - string? testDataFolder = GetTestDataRootFolder(); - return Path.Combine(testDataFolder, @"authorization/Roles/User_" + userId.ToString(), "party_" + resourcePartyId, "roles.json"); + File.Delete(instancePath); } + } - public static string GetAltinnAppsPolicyPath(string org, string app) + public static void DeleteDataForInstance(string org, string app, int instanceOwnerId, Guid instanceGuid) + { + string path = GetDataDirectory(org, app, instanceOwnerId, instanceGuid); + + if (Directory.Exists(path)) { - string testDataFolder = GetTestDataRootFolder(); - return Path.Combine(testDataFolder, "apps", org, app, "config", "authorization") + Path.DirectorySeparatorChar; + foreach (var filePath in Directory.GetFiles(path, "*.*", SearchOption.AllDirectories).Where(filePath => !filePath.Contains("pretest"))) + { + File.Delete(filePath); + } + + if (Directory.GetFiles(path).Length == 0) + { + Directory.Delete(path, true); + } } } } diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/_testdata_/example.jpg.pdf b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/_testdata_/example.jpg.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5014a6a5e789f8404a1ec4d8e1663353240e3bbf GIT binary patch literal 63943 zcmc$_2UHYMvo1QKpn{+vIVoYt12QB50VNC!QHCU0a?VM@fQsZKLl`o`Fyx#wO3q;z za*mP_B*(}9-}j$$*E;K-bKZLE-n&+`B4Q!}f@kE^k&!)lN{=K{Nzm*5LcWm$;KDwj6 zCI;Z(;N1Hs_j?cS`T_naf|!AX7x$n1oxYHQqch5wB!7)8G4sLxkR9+#IcMHZ1D@RV z#UZ{=43GeXeg;UqOXvAF{{NdRl#>9!D*%A-^M5J_Crg(o`Yk|Dj*k?k#EK%5uYV_7 zF-Zvm0Kot0xc@*LdJDjJd-xvb2b&Ee=@w5d&La8o_u8nB|Dogm?glnfbK@U?JGoT< z4N36N0P)sT$-v^D1U04P$Y{rZjLB@o{}TU`Jk&N|j&W4Eg2`bwJcdFd(Bqw&-43K$9*&A4@0MDn{_RvoH7r2P5WAaqs*8ujfXy_fc2EC=dXs z{ccW!o9#u$oOne9nVxQl@DFkfa1of5Rmo;hYHli(lz@9p_des~tiJy*^vM6qz!&iJ=luu+8C;DFtF+IwAtz*7 zU#y>cQE=Y1nnaA{)eaVk4Y2d=36sJloJ?bQdPK95s$;lg&v`p-Hx1XNN7`XIA80?M zyTX#Kz_BH)e={7&_H#U`*K|^SR7ZFN6IHXN>~=6>?4%k~SH#vomLsay*@2tFlimG6 z=rgDHtQcE@@*G~mY|*lDJ2{n0Z^{F$4~@z8VZRIN#h#xPIr&EKNQDO}guQF#{LVLtrAe0ASd@z(KDx0qPZ6A$>NkWQzp!uP?5@RB zgzhDha*R~xJ)w`VI|EZT}gg{d`TPom?C#7eW8Hb!w4-& zn<<01tk(vp7m1*as5(y?qN>50|<=o zlqcTX465|fJP!)=!x9=wJMQ(OjSSZ6j-20RL|xh57p`WPVd}rKPUN9GR%6BMf=;gK zKYy(9ALVur`ITlwZwLx%x0K>Rl+UGr)bOAE2>#&v>-_7BH)aNxf=^MYd+K%`x(`_8=!eWD2;` z#}UhOTr-IIB9vhy<_4?A0z*rtg>bh_59i{%o3y0}^ke>0swf7KSFmG4bf{ye+ISZQ zkM+0U{nn2OE+g)Srff%E!v}uPtYczRn2az@E_xKxR`Poms1d(CJ{s=i;>Xy$=q&1m z1b;h}YGPXdCn0pugIhqo-|CSdv~xSX{MC!L)-G$eUv8$<{cdO{LVLk2Zw3QdZz3_i z@3cYnVE(_7R_eHU)rqRUnb4Er<87C|2p^do-F^|dy5a}Vtt6jr^J(1zqQ?^zTq0+6 zzbG?aRn54(hCpO7XzK8&+D+Eq+M;6S7g{48oMfe8Uy;@mw}8#$72xV`wA0B(aTAs6 zF_2q;?&+B}`IR3I`3)Twe@s@Wzeo)QRbr@6E1z?yfL{^c0+?K#>#G?1mWv7;u*@`W z+9AM_+!O0*zl(6)&-@`L@q&#dtZT(|V!YV208Tl+iBuvt^@3kzZI^54i;dENGEb6} z^fkl9k(AOpr1S6Oh#?Ik89Vsn4Y4ymCTdS>Zq$fnP=@n>b%CqKZ^46RXXaXzcG_tb zRn+S~FCMY3hOQu_p2<$stLfNewi?)|dDV$ZL=?Oc?&620tBVZcykPP*bA6%QENc)# zsAf}{8*G{;#QZXLD*iB~d%@sPD&IGb zAT_g8%6yHwfo+TaPgQaPBocj4`EkFU@o;zS#K~ne@>j@`;LZw>WrnOZik=Lm=~`(g zqk=-G?_P!zF#Qp9wKnFM+{y}_;~&`~W*@Ag^88yiSVCIVoE+nSo-`LcKKtmqB{Q+- z_2QAHO4}&JN9H^*eU$$cjN?basgpUnEr% zKhb`R;giS{IoaddyiAzImS~1;K|_zfXKEC8nw$MG&bH!liUGDag&q@uRq+wkf|Hgl zx#~JzyCthyxu%1?pL5>l{^wpPo5hb;q--*!e1AB;zfuo-d-rVvystm^z@WL^VYTZy zYOCYcila;5oHY?RyQG3Qt^5Wfcx!XYQ^JVe9gW!$PLQE)3 zziAHeA-EP`z*nV!pYwX9%6qBEqG+$Bv&YK$7(qu@VdV3{rC~9;z#;NaEk^qB1a)}3 z2|IyCFLa|3CA(Q+tAQ$BhYJML+ziRGw4lcXudvBg9UY$vD zgdTTZ6F5^y*oVb`9s^Hgvy7NFP_N{S@*^j$he+7cq&KggT6ZK3Wau}_q7Pa*Rk^hB!|X@LZnj8+s{G6NZsJ6 ztXPQevUJf<(oLNF3!#!fF2#+OoASzrQrZS*#fLw&NkRt($nXpkB%3tDHnbgjo=n7( z*6w31-I83ON2VPr+BseCXIg?m(5+69(!DHjU4BB~Nwz{6+mS%CaTVvY(yAu)JQ-cC z`dAW?zB=NN1drYh{R*F^oMXndF}7poS`|}kF#pXD3ZA+!Sv2P}0FGGr*CM9eBXGZ2 z9A=YtzJvHLBDyxEnpxtH4z)`v?g#uq1ZW6pj4=K(-8sR4q!iZkmL zpfam=7O%Br8H;qDXge4icT*^5kCPzMG36kY`A{{g-#-0Vxjo^4Emh2+%wbh3tiHe2 zY1dbVd@WlHPnTm;h}55%a}OeRb}oKU`Zq$eIkZTdZ$*($#AhNllOHiuMtm|Jte9-` zow6ieeZsP4j_F!JPs$*Pr3VA6qOvhHPNG=<2wX1#Q_t88?~1VZDr+>yUorw{?Xm_~ zAThT9C(0TO?D?Lb6Vu-)0?x_M@X+Jx^Es@Ld&yOfi=DXK%_`H3Gf8t6JBQWkiLL^O zT4m7-A8g(8H5E3^zLVLTt~i2KfMjjT%zo+K8ewZB+Jfm$5$|*whVa^X@aaQ;j4-kF z0IPZ=FfzA*DWcJ80ao@VW*dFM568F_h8Ob8BvA`LOHX7oD#~y08cPS>PZo?UxZg`W zQgVR!U!G8pSqUIvu4aeQg4t(=WFqs!Np8)F+{(O4=|3Hs!`0>Z*&yCz04l+MxC{T4 z8ULT?_f8m$V~zj*c|Epz&nWF@^B)FR9k20^+6)H?lk(@yd>#Lp6ANeCf`ET7bEVZ6;YFE z0yG>h-!Jde10nZt$TwV~Q)$m!RH(y=u#=S^e{WK0mo-){VP|QmMx5ZJ6v;%1=&~11Zjp3g@vyASDV|V(l za(1mJvscO$lr1iCz_@NH!+343;qoa?2$;xud_rpo1p96!{i#zm1h`W7WaZCVjZu^P zY|R4%qlh;L3tO?ZBR_mN*mc&&y7?$AP$j=Uf5KO( zNAF6ksP1elTH_Y*UIwiR>{93!ib27`gO8+?w2|bJ%f(t~UXA<#ukh^) zRUZRmJEzIf(=FgWsPvo=agYiMj^+54lh?nU$AXE67}h;oh;3=tNpyrI z`1?&3X*`=Ep#VV;6JPm8ZUKy=2UJHY%b`iJb9dXQm}TGj z^DeF>TcfyFE>nU-f}45a{yK<-O##1NV~+Q)WG&^_Yk_|S@=#mj9X&jCM4s>0CE;>$_s;T}FcISe%(9mWd9s@@1!+^W z9S~FR)I(*l!MYd99czmX-$5Mw(XJa^)G(APQn|CO&hLFou2Wi%IIdUtJ-_$4m3l(| ziNeYJJR}+D3&3W?cl3}lPS%FKwYXDF_r>rl;R=l6t%0&Ty0rb(Hej&aZb(tU?|hm|R_$v?o){D_g@@$00RHLcU@(C1~g* zx~$_a@sPKT%mUM%d?NM0%dtmc3w*+3^!rMRgE%yw(;40}_vl2Yv;7bbMu1Pz4Ib-aH)5^pn7X>U;BuzwK@uu?4s(`WX)`D^+s355t zUjbVHe;*w;MHP9{5$1Buq^0;kcur1pfc1(cHAP#WiAmunXiOd(QMO*Skz>A5KMiYIm2#Oxky44SeX=6n(9*9@cGYLIzB|E zkjyzZb0(@ICR6n~INY<+wyEol3cYk6`7Haf6g0*r`5NnR!h8P~5cfA-NkEv#x65*w z!|Yo7(A|-T6R1x)jkK2SBC*el#VA-Fuk4C7Qn@|*<>{%_rzI_i;5=#F-PTWi?sTLB zy=mu4AJUz_zt8P2i-OJUV-%(2Rj$6DfPBV04k>77+3QV9t4ib8A2FXWUI)M_Pt_15 zhxI0Zz4v!Knzs%)Yzl0H>7+BK=sO-7+lYrFlXs7}y8DCOx@yCwr+znh;53QIWy)c> z$NLcmE24y`7O5{B6{=B+Uwg9@5t&kUS=Hv|LbKN9R!jX!N^ra82DdrTMh_EAVD-eX zw1gb=w3i$8>9w5YX0VVB9P7p$D=kPEGo%w|u<48R8#NjSS){5xzUhGjn`}J|pHE$&#xj{fd zDMg3*uqP1+BzrN6aXv&J!pT;Br`peM0iK=C z3Uum^=8U097iuR=f0$(!z8*O6-%!X5oy z-85LO(S;-@Nl7vr!yRAcI_ew9x=?9xz>ze$^ra_W!6w%wg_UyS2VP4Q_UQN3Tz6uA z&IZ?<^Dw3xFPv<}G4L`fm}|%ARA$EZ>6gn(xf^R8^x|Ycaynu! zA~Ja%q@FM|@C40ueVWH*4lOe^j*u$)Q*JNj?8uk4-E!;(aNg>Ihndt*LNER zRwlPzr_y(S;Bh?3_*gCb5^$g8Cjij>Klx?)_y;J;2I@GnvZI%tnG? z-B-8@-T6aMGE&1$fTM%OLGg;|LA3}+;{5ct?D%t@;e`1&f% zkRh+QU{udE2X0SA9_8DLYG)mhUg(K$rmZiLZ8`g_k=2x(XUm4I9DSx>8h0}aqIwEJ zBJCsfHSF|U`^^MYKHKH%22937&+p7%-kJYoO_L~xeC-6`rmH}745v|yW>`(?v&k#E zYSoZe*FDiTB`F@MR)$Emog*4}l+0{HlqjoCTk#~QPr*>rBf@9k!!hZMIcTsj9VZbl zE+Q9hO9rZ}z6In=aZH#nl)l!6cu+Vc?bM}?nr;&aVl7%s?xIeTdO_a3&HM5Z_IDcn zw*3A<_eYxTFAeEaEaa;+kU>w5{bG*S^*N<}fS5r8_ z#~)&jS+gmtM*Lr+^y~BS@8+vM!(UzL&Icq9j9zSe)%X3?R?MsvWFOW&XAlO5_^!A) zAC`t&bc897+yC%w2+xq`PCZ7N917q)T!Q~f-7%@#oUn;~2rB}|GWS{h?(v`pKQH+c zKjTw1KE1Hx#qQQvxmcYU&04NS4m7-qG-1*#wzR2c-#pe0!N%~ro^sy;eum5zcH3IK z*~I@M>7@1{+>99CvRVd358n-NR%UB>Idd1-d>`;Oeo5&31iKRA#NvAkz%lsQ_NgH3 zQ<*0|kmgzoENXiCJLVV!F>AX1_US_@>hbY$brqc#jWW^N*l2FYPSJv#_2`ckZ_*TI z?8#{UJKy6&4**T}TpoxX~y^CnE~&3Ox2gb$MfbRSW@ZagZ&M9 zias%98w3f7{e9g2yvHeFbl{PE194Wkd%JQ$GV+tG!`2{QY@yRiqQpuWNEL1}4_>tS zSVFDF9S6;nk3M&Xjg{{=w;AYMSudY5QpTvE>T^l5a%Xipkef$|>E()@G-pxzJt3~T z?tQ_YPCv>c7m$3maYeo*ILuNTktLd3IZP$qJbiDhZUGsTof1JqjB!3(*3RCy0Ic!s zDFJ%yMZojzXi`pzNFQqp8l8O>7{~sF5}Jnu>n$M`uJ2-|6ZG`#!1`SPVVwfJ<^8ll ztQUAbJY1^(RsT#E|MW;s-bJIiDMybWaHal;Z&RrPf%>KL8O0(P7yUp4W)`ErbHA>4{vi* z{iM|;4a=*b3G*GYjUYmotA#olTu_^=DbSNk(IDA1HKL7kHSu<1I{94J=fEXY{jRJ@+z*S{Yo!5+xudenJ6a?$@S{|@-Rwq z4fI^Wn^08QEnx4JBG72^H2Ku`ef@Gw{5FPSGhG&g5IddtnW_>t^$2QEy;RcR8$?iQ zGF~rg<0na&Bp3r`0w0uoF=E{s|At9#r+LAbouk`1(Mr_GSTo`$|+o|VEx*^1?7fOg7zz`Y36O90+GNxsS4(#sdo zFusL5KzWgTr_KDn*-tjvS#`XhfPV)MZH!Zy@8D~jw*UY>)fIl&43ypQz7}w@jV*pa zp9S6wd1v;a22ZIlGs^Clvq|XAO0al0j|;_016~<&FL!*dv3LIaa(9YrOl7sfy1Aso z#fb!tt;JFeD8J*caH^NIw0QuP_mu~OY-&n@ucC1BPMEEpDQbPDRoJdK4)v>1`Kvvr zhQhdx;IfjmwfynvvkRm7#?oGv%K>TwpPXkUQ1;(1QsS+~eg2Q42&T zQeD`->_0s@CAoIqxgjulOuF)YPWdWf!u-25n!((V5q0s&uJ%ex<>VId)laWTzVB1$ zP82ln!|CVhH!@<6$u2|9TV6rI0;pAEy>D%>Iww<1Nu%4nEbOc{(6+MNPKlx}y*t3RVhEz?UHutXU=!&C>fBwMqp%qu264JTFja@f565@$*N{5S zQH~f2x!mg16t@De8`+U&wZE$253c0lLb3CaJ?(6XzcKcHpj;1dd&*1j>%dkGCKELH{(=t4K_H{IS_T0c9r@A#Q=k=yn zF3_fCkA+BWl?>U*F*AOQj+%~BJ$f{ggkoakT!u9O`$6 z%h~(t8jd;;XrgeNWD8zl9kLa2R3i_ zYvegn^YJOj(lJ}{e90}@Np@MC?#SHduxT&32ISWph{>R0vh#@P^!q%4pv_&0N3l87eB3QL zJ1i<3+gIpNtk?Jg4dYfY#2!1NqCeQU&{q6}gA97&eBULf!%RV!FoP9M?4TcRWgyt@ z9Ob3C>(OIhe@H$;oE9Q``J~`mO{xyW#y#RgUUy1|42l~W>os+GN@g7zCYp4(NuqJ? z0FzZSm1&BesYnCxf;6p?vP6A z@VgI#oPRp28zYC6lykh9G7Aedn-hqi3s8pUPQyjzt1crwh4O9eK$Y&vOQZ_3Z-ff>sh1i^sDX6)*NJfijrgz}kc@5(AU&$Bum6$;Di>3oh zH0;p6*(1M?kdqcidOe}%QOXh4^S-GyK7x_ys;kc-)~AR3>9loL?kkr`Vz|~`8`+AE zNmu#Rn?J&jy!R)SX!b)UlR<9=oXx-jmdfPt)#fMu+ntTCjvP;e%@ zH@6OB^SOw*`v|Td?zDbh@35}6%L~$aAxIZrd%`a(!WFtkz$K>c8IVf2=4aFyX30Qj zIQ6p9d%+LK(aHScOZUz@?JkelZ!L}p(p66fn*xg!h%vKizmayk5NW=EepQU6BW5%o zL=w~7HucF})<^s1)6Fe_Z_spwV>(4t+;6cp1?0OD?ovOs5%qhr>v7f%?uCssJwAY9 z7Eh_+Gl1&9m9z7&O$si>#S?W^0|2e5E=5+Qai9*FN-56DpVujT;Ndm$B4$&-wwIq~ zSB34oKm$t|b%A7jZTX}syO{XJ3BA=z-H~I5Sx9;AWEE4BW^Z;1G}XcvNCs7sMK8G9 z3YUgK-Hl@vRFOlk-O`>_a=jZaa}cu`-RrKw_QD1ApcFox zSvO_s@TV(;>FZRYNY*a9m`}(&&?7E_#qG})9g^jeY5YoVdqA;qH(7*$K0gyph|WCJ zW&8*HJcr0xOe2BBK+`mL9jB#wbX?_OO?@qY811c!v0x9-IEEJZTmui&m%D%W#3bL{ zFvT`R=9zpL@8BR9jX1#sMU;!VS%&@cZR?rR85iVVpqnzG&mFUi-kWbx<8Gh$7QWd| zUF3dpl&#Lx<(QnbNuf$bywTaRD6=ARRn8MeUBB6nE;Aj-_)|qdYoB1KB zP-_&6;TNv)+RK1)-BHp&YBvWK&Q*AS!^St(axiTn+^@_-%8g`Ah84i3oF< zFnbk47^i$6Qn6|{`nyE`6;mpk#3)$LeUmR%I!g7)ar&R)b*aJTGg;&DgNhpW*Tu;P zx|Z`yn)ON|lSjqeLI?ZbNxS5S3g1<3L5d^e^`P{9v%&q|TAczGAt@?_x}goT`O1lv zU7l-}L*6442a{6S$LgpjR=uFAKUJMgo}%)(gLN~;3qx3pVNg{%UQ6QESdgAV%gD1L zw6CDds_~S42@Tg_7h1$i6obL2wVZ2YG@dCqye++p@wm^IGh94hu=0jmgO3GeXJr~S zzOz^RYI2t6C@Y>M$F5{e$NH&^HMg!4$4ya85$Wg{!srIsN2$C(E!c~k?>wB|c+2D# z8a@8z4`3`hu6@?3tku;o2cLCn*jtz-3X>eXtFZd{Y4DBPi9=RYO*#|tD#TY@&)}q^ zfP^!)-l#G5F2V>mHJ6k+w7(4N=x37b8ZVFwQ_JmS6nJ@bXG@$yy_||V!mXpOXM|1+ zvNjd^ox721VHkt;^4~&?D0Nh8ry$L0hd5^dJ#0DJN(bb6@^{g;Kk|^52Y0EAL}HCT z4Fg*y)e&&>6kmQcg$+3sICW54DzevLR<^To95OE#@R)D!xRNba*%Wjuu(`1e8DGv& z>408FQ@|gOb_8getjMMv`WbV&T)o*#UyqrX+_<3`_n9#sso&Ek5L{#r$c?q0kCNSC zR931r)tka;59zvJCTR1^@1npvQon<)v}oOAE1V!j zH|wA`Aof*#hwP)~?k}mi^($M*L)dc3 zzCsD{+2%xTQ+GYT<_^*n(wV7E&W6hVzqpwvpt()Hu;tAVbM8xVxl{kZtcRZ*E1Gr zJ`0oh$~zvr1@)3wN1ezMn2S-Us^&BSdwyo}iCI31(&3#}AepyPW=v(nq>ET~sZH?b zG4EI;@*Qlw>@&gdZtkC&V6GnK6AP=F)Pn1GH{i4DaJLIi_4#bz^=ohW9c~zw@0Qcp zk^yb6Lc;`gcXY_ME&~Uu^>F4(CPf;aWRx$GGEIesC;kr;#pb=6@&0ZV0l4j0tf|BT zj0bus;DR%sOQ0^a%tia>3h+*mUawfQj5MRh@Oh?g^_h?hFODbtOC-anYhVjw3x_7h z>h_*;$2Xv@`sH)q8o^(SW}ky&*LC7Nl}z-lL4{)_q9@NHzeqO-3&(p5UmmiGVK0AU zvnta{a90~Ck#FKwnzWG7Ae*g@7}{&~tSs<${f0{Iv3>IRF^7ish;V}CP{cQ^-O ziULx3F8q08E~sb|bu^)IxPQpQT>^9NUM46PBM59UbrhwpQO=Z(;yy?kd7h+7ebL{T z-u#WAfqS}BrjA8V>j7GVca1t2d4rKKKB4uH>CkSn`O8g!$jWnJ<}A4^nAYX_N@Y*d z>OtgFPaU9Qx;#Q+wmqn28tpK%s=s0~@#P_W-vvIUVY=I2ku8QazIyy+Yc{-5O+J93 z7tRosuZ~bLfxd7i8R9ZeHVhVg{GpnikGlzD<#dEl9QN8X_hahqId>jkZpvBHAmzPEt2ceWD!79o@ecUbN)yRe zz;SbfxfY|5^})%l$s_EU;_?BC;+mqmA!$4Dle<8f$J^#2Qo%LGOiMQ#%Zqwai;6#D z^zX61O^GCQD$L|rXLAthGr2qE6J;A7wQ$F=zxs>?k(@_k&q=GX>4RcTIr>dq+)8E4 zBL{T9F~gg=$pYk2vWp*9wDe3~%j!w2hPfm+X%+Xmtk$n;aS|@?dP?l~o5q_1(hwtUE<)oh|0HZ1 zygxZq8Zxf+3_SK%#<&89S)NAzV%UwM=^=@q2yf+P3T)7W8Ip(0Hj2XgHji*x>0<)< zxbyb4bLVU2sW~Xv4T}1vBv6Plzelfb?2T{?UNouOOITk2Ply4i!vRz78_T?Q| z-X9s1r=ArP>E;uE9gryB@`C@6@rgw`TdcV$FpI$=yF2(Qn0&sl0euoWa}ocPB9nN+ z)3l0*x7|Gotz#9qWFg zW1G%T5?RweBFcHFCu#?swODKv&1_tVb&e+E9x1Xv8PM#5=!cc|3R884X{I~Q)LdmV zbr`2e=RK>2%E$daKP5fYr8$0*YYkga4k<0@vDawXyhtV>KCqyAzs=sVGimU3y=MDW zUkH=D;^mpx#=B-2eTrk&XfS7-tmz1PRG%-aUhP+^Gd?TPHa(Gt-BJ7s5|ZOi^{Ozn+|t z6ie$kyngj2FCrte#VR5(dIy1~@;jqb5KB_wb0p1@XD)-kACPlHpSTo>%k>SFMt+dx znx!@Fj>G4Mit+90ofcRf5FQet|$Q=NDLok1jYyPQlL?{+xv)X^m-QlyS<+8(tAv+p6{|7#EWL-Mh+;eWz zU+k;SP3ESf?wjdjJLXnMjbwb&vyN+IzRd7%huJ`?@VcpJr>sZVP3aEb zq_~_S*bVw?8&}@>v8UU+ z&Rpjc`OH`ODk{#=0XNJZU=A{CLOhNcmPg;w)GBps7Bz1=4^AJb;oM<)*~jmS15alP z;O%;AGMh?uF1to6tQsU~=Rc+x_q-&GI5)MLL2FtsRtd5^iT#K=m53VXd zCn^4lqM?HNuD!1r_x`suuEfV8Cz#gZZk4zI*5A@?Q1_tG~ zDN5Ll)x7kbRL2tnrwL3=y=Y|r%vL4^-IO&lcRFTuShbpU5kSXlG9?x}6FWxM*Tu~m z#8rW#>^C{13uTluB{$_Wa0WWxror7~$)tsW@^>zRx~~_)PvChEVy8`GY;;u0wq?rh zJwhNxs5XI_&z~M}G*GCvu#NB7r}wp*M%?9RzhI!xJdy@AQWL$7j%}&iKW6`Hv2F@2 zf%^uauQQLyF65L4pH1=aXa(Cp$(c|0J)Hd}1-oT;^}Zvn$OZLB|f2zv8~ZRUKiSopF7C z#VN1>9;vep^IJu;;8nNYgp2-|rfbnDO9gSRQTF$*OT5N1@|JU{pQOt788Xfmb%uG+ z9SElCIg=RcZ5ZJT2v1^BJolSAd!=))gzGtqxez(JU{#*ZZ#IXV%?b4jbWq>$@%kLK z6yQSEx2PseO++%PlkIVi5s@S5cY`&ZL|;QKQ_Xd! zGzZ=%*_>!H2q)cj$-17xOv=~Z0<=;+1O}a3$!Pgt#~0Du3$aa|2-ym-1P%4CiAk#p z7&gB9y1#uUI3ckiWRQ0G*c|qJ92u*74LlU^z;|qFh_xOIMUN}hL9-DNP{)><%u`-B z1Lyn@<%QkF!cWfC^Kq@?-OIBHhXj|(8D1xHbyf!pG6xx8dp%ZvXT5XVVeWjl@)e?F zILpDyShGK`XtuuzX^`;2c7;Zr-)g&i#<@jDR-MK1S&0>0rtGw(`V?z*N3vux?N5Hj zrezMF7Wc8#nW;w-I;ZUjbvGblik?z&0Zk$PTgu0iIuNI%GlOoDaMTe zLow;Z6~Zhyg|ebxl00aa-O~?Z^ob0-lzDsiOVwT4Ty;0#UOxt*%S0yNcFfSm2mQXp z#0Z{TSA8BLe%&n_>(L$&n`DJ@UM_|$e6SxzO8&S|0iT+_-m11-s3Y`n)T{Ewl&?9j z<%tP;9>37dDObJ@{^Uez&8R$7I^@K{m7eI(O@75PtM0~`FBQUrK^$_d$xtHV@~lqH z;#_JN@=q(KS|@R8(rpxh!Gi+!uQJsWtwGFv3$+O06_M!_?^2zvvn3GO$7Gg;*b$0h zuaR`E?)^gwFzjr}1kx!;Ffv{ETns!uvHfnl`DU4GSzK&(YQA}OqRAMI9;}O*xKXLi z3DfpF7JjNx+&7!ZntiZ!S@El~(58ifTVIom1Eu$UDQPxVDXDwtlqNZ7>eB%PinP~Y zq!Y||bxlT!*x*l$sPH<}{L(36Mn6!dMxT{M%wp05I`T59%otpE+N6LF(E3CAZ}{XIcn`&Kk63{nv35|`q;GHdaKZK* zb?HSMqJHv^4HWwu2{XDHKNMO;uD4%ZS$B-^$Ns%mCh1NQzmS;hL^L{hKM(mdra4xa zuw9>GIegLA5F+Nm#~bKNJJB@YZ0h}Gx~v7vnGbB9}>m)v%72V@}0j}VOF^=bQH6yt62T+6J>u?Bo4GQQ)uRXT_YXmSI(5R z9QC`5(BsTsItT0G)kFcrq7ibDZ(R6TZsxwZMA@pZW{r0=!7lt}E%_)ue8Qv>-_*z> zL5}Lm=MZP7!08p=Z|$hV`(fiGgEDyI@%3LlHj|@OAb#D4dqeH=+wKZVI^uTg+JqwdsSrn@piUDDAfs3=zXoW2ZL~|$OtA)I4cu^ML|Dmh(VCe;T z)9u=msj#6i>8n1=&z{DHmU2?a%9u5-oMj`anT_9~wS2^ylO|&WIhi?19Ciy(uq%R? z`>n#?b@(uJjE~BIpqv>*FJq&Is9wv=32QNp%5^>!Tdb6xa0OksXiyM>u!jfudbDxKs+SV0lBGZqsixo|h8>D{U4#2C38N zNXq9xyW3a9V-Bpqb3bI%Wb_+QH#w(fVINBY6~9i0Dqi54NLu>)Auli=wS(}%&2HN% znWW^BsCs6RHN0*&I*~c5%bA4au%|Y2t*`XJ3v^b>%zRC5^J*kVTh%){ifE@_Bl6Yk z=+1Vw_CkDCB{FMug_TxPr8U-H5SvxbzR!ft8;tXCv=F{-NAm(zPZ+IQTgrhCJj-7o~$Vw zSA`W=1!#VXn6QG8WielE1Y6VE1<;11Tb)!(q2-N&i;+4Q|Er4BKK=GYS@J1S6IKd( zo!yo)dU4;Y>V}I;ny}BWd#10uoXix8A2YI3r-UCN#G=Wg%*p}}QPCPi(yQiN+e{O_ zDZSUvH@W-7Hik;$6J2;$xV)(adGbVA{FU-wsZJ-~qhotVWmDf0Z9)IZ#l+}XuFkG* zPY<^Ca?a@dZr>7q86Uv!^06r1Y{Yw)te8Mnx;We_?962L?^#oqh!AyQPmP_ONWtr0 zjTb}5%vBXUv#zgNn4L~c2VL}YFv@f=5v9sLH!+rSrtyB8J^i(U%ov$Csbclel;!Yi zK@-j!M(-2ezF)MDsZsXod*^vxT${|)VwK4UYZL}bq4zDJ!zoektx0Tb$=C$78XcU!SBNm*UBbmBsjr?equsWa-82 z5nJ@e7Iat@PM`8MJ%*#Hv5}}99V&?RlG?eB!o;9(3dTfzWIKiRMB3cf?e52NBc&=e zttDsrZJQ-iRs!0qi>&3HbTBi_vne0SLa<_;@Lw| zIlft~7h-Pd)zxnXT`d}xrw93|$?8{w%Q9=HqjJjir%tas)%v}yx~#{=?aCIL^qhi{ z)pk2nv~*>QKG7)f&-_1}y=PF9UHkUy<9*vuR8V?TdJ&KkdU@!EDxpazQF^FS0@81# z2m}a(Knzs~Ap{6W?{G^Ez4s=)gFxuTops;q?{En2l zpYKP?+HGzRDY2?67-kqm`0P`J`G51NH8IhP_E$`li@8+6y2V42Z}L}r-Z~*ZRCxt! z$1hxDzWSReZlRx&>~o30Ft9C3of5hV^*_$Sjrhm zY;!PQR@-Fq#_6GqP8Y(uR*>fH|KsM7yoInS{4H$o&uioVo2KypDqsKSga4e}y7eyP zj?s?DP|*MQhFSEVTXzax+Q1!T)ii#KZDQqR05did_mgwPxmdS=Oz;5BMxj87(d&Nh zL>_f#Vaz8Mzqp1NOlN(8`r5B*Db`$Ob`^9mwRWzX#VI|RBV>(p0rBwHqKF}9QJP@r zLq0ZVbf>j`FXAo1)UvWzws1Nveo@K+d(P;qaV1k+pt~H#yj&Mu_09jP%+`SzDy);eY%1DC0j zSJ@#7Si;A?+K9rbLPm!Iz0|LQ%85o@d|fQ$E>1^C!iHJ3F!2-ZGHyCTP@Kas^$pn} zj0Ek_++L>jKP@H@p%8!??syN+6HahiswtPqzC zON%YFoZgNbT7lD~K?`;m8hVng?~{Amd+TU%SNM36B~DO#<>ez<6cOfO@f$F}DGr^p z9IRx!I&uuoEBJM$<{aM79^!4f*6w z9}kucm$3CZUR|MjZzRCU?^ckp%WhSfSq~&Q2V4K#qRoFLLjIB0qqCzLIKpSWFlc?Z zd)2M{_TthpLN9ja$VPZ_3Ydh*Wi>tx57+Q3pOauAMTG!g1WOBEpj+Y z^F}unuIMI*kTZ0t8BvZL%eZ#rHxYpWC9Y!V@oxr#waWLTv}6nNe|&0ooHk5H&7!B= zfY7iBLmCFkg#lN6TcD}P>m}ldZwOem0K`?}qT`6C1)nhah7Qfx?%P$K?D)X-&-Fs> zU^zTqrps0f>BZ4qu+DetI4OitGcf4s#7hyCAs z9uIGlB(!Js`vf_aR^*MtyH*VVxdYPlKHASZW_0u~biUtcw%6)&(92lriZ5**Z}t40 z!L-?r3r*~5VR2V_{BODFbp4L9c>ZFYg=mT6mFuUX*!U7@9`EKz<<(5Ar*B8|?pM(* zztCs?{cRm5yW-;G)5bT+3+lOvG*3H=A}7mbo|u1Y?bm~jS5@#eA^trIrI{kt?)0b@ zZa0K$e?(;8BUdGzinA3SN2N<2}wcBW^n?2f||{b{y9~Anvhhzua2Ad1DI3%CDuI z$u->1;cah~rtH#Uc`4|G@()m9f+FHa{G?`vG-X6tTVMvlUVUR#j%DIJu5aovp^M6Q zziV7|)tIQm5<92-&)FFkOC4IQ99HTbgeyfdr{k!LnsRm!W(U zl>`9VGlb}-5oPh{FKMpvAeq>bZSuN5p$c5%v!}m6U;^SQ!Wrxz0 zt+bN-?OTe+J@ZiqW5)&tn9#fTB3Ibbe)dG4F5^d2BA&T3`E$LrmaXZ-w!-s@R;1D} z0uMa`h6-)*y)#YY6(8G)@vpXKp@;O_PM{B>b|rTf)FxL1uynttC$fvW0U(>Fl{g{| zhAA-qb%=-Bgp@(3{hn8Ov634i`8oE~PO9^)j+i1SQtAPUKF@Vnsxz96b?_3DU8a`F zkFEUJ@%7O~9M2SzB9$1_wtk`xkA1DznP8efGH)LtAm9``QMQj2i?-m1w!!BVa=e&w zHxUrUWQUJ1UtIXz?4msvxWs#k(rvX-whObHR${o$rO{^F>17U1te0uEQbaJA+~uJb zWq-cp;xffe*+aH>0FjJK04}0AD%;uXp zR-d@k0#|+Fln~m?EewEk%*u}ly(`6h{`2Foo^PIC$01w%(zS#a3!7r~3iw8p7x6WE zyIJApD;?sYnUKNB%NZB9(LcA`ppYF=pi7riTWPOlaCwP^u-~3o;%2AY-Q|i1_Bo(S zNDJS~F@-On86y`iNdhl;2wr6Q&91dT!KE&E$Q)*EoNoU1xhSt-;K@sQM*!(R))5pX zU<5Va{^YaC^INxHFg;8VRXH6xqrY1pt27&yD&H}(TEa3o#pumveDGKoYx&fU`6Z9t zY)aIn0I!`@M;XXq(r@z+v}D{99sqGe{|HM`cMmg|_+mm|w_ec|+|wYK)-8JT3S8Wt zLg>Aq+(hj+zv=l@m?Xe(k7e&67$fj}8;^4%^IsU_T&yBhC||$_8SqRa<+go9S))7Bb`$3-zu=qiW-2h z^<0MQti%gbq}JPQrE1p0#;lCEnKs?6e#RQXk9++Dsa_O25_L@bWKU~H>aIs;LOCx> zSR!!&=(U)e40v^u!zpsf%yR#OT=R?V=!gfnPAuhK_J}qG0`%K2pL$T$pI!v@M*u(G zXi-^Bp8AYV)t01V{k?B*Y9z)9Y~)Px`{lXjS!`?@G$U5*F|;S;pZwjb-6mtM+Q+Ma zqTQjIKt(@A%1S@nHRUz7#1=aj_r79IbrFfs+cfMccd(!By}XJt1mL%iae77CMu+w? z+5u|oWt0$9(Co|`B7 z&G-BmI)yB8dJBQ_rs!XF2M3tb(t1rzqa2riQq5+yoyBH2E55dBe0GuKd{~OsWnUo3 zE-znD2X~<*1A!?n!xj>vTfOubm6NdzQ41PP{(q;}nXaVUyKzn)Us$G&8gY_O*8|U( zq?tGUr?1srGOvmx^utfbXaA{FvH2H!sbm+BX#_ z2#sUuHbf^~K5?|#ZryrU8e)$aY2Ne4*q$F=sV}I8w5fkKTH)pNFZem^GWbDa0ros= zl|*R^efZ&~+;+!Lp?##xr7jmO6%-fzh*ZlER2kW zPsUbee;X-m2wJgYNCbsFWb5HRl?LevXk9sZmnknKX*PMTYng~N@*4!4%D)x6`+P+> z%LI*h#>>_b&O9CU=axM)<)zU}`N{EIwlg^z|Bm{)3?r+SDIOhV%1MIo;IFnDzA2f^ zUubWzpSh)HCKor7ZCl&#DynwiQM%clX-HH!>eKVlBxg{Cor9)@*udlUr_iW>_hcKn>K|*gj)k7J>{w{OqPPR8A%yR8!{# zG_Q(skWx+K1rdG+Zwd5PCbV1o5~EdvjT<*|zFdxMGGmaYL&A_qjPp`Up6uv3ky$Mg z;^FCN?MCA}C5>kX917Wn*%8B~e72mpT*GhM954{?AYe{bZ*fBRY+r+)`SW|IE;_-hy zL%t@9g^|}LRkse(j=6NrV3R z<5BljSR#W+AT-e_k^GE`Yl)^LEuV%}M!E4uAGke6#nrd;Rl}6JXcu)JKQtEm% zgIN^u=SeWxwA*=oZLtwB7nri*s^#IGin+T{y(RFKmw8wp=Mb>Ff>2`~NDoth+8T-7 zeGLOVT*rycs(VRDUNYS*0~Sx%Yh}jt^UDJbY$T!Gn<_8JUzBZGVqnpTQdYSpbt~Ry z$$wW79_9;BY$m&nJl1diTXtsVh&q)K?Kyf?XS}s#8M@-aB0iD*@6U=Z|6e&7hT#_- zsbAnlu?;?#J62J(BeBJSTkHzoRWNtd)uXmxa;or^aP4{*vvs+{#P%Giq-uP*>O?32qH8Ws6hf^`BL-DBem?vL! zEJMT_yiq1%p~nBy`>=Rq759vqtuArLL0aS71)Lt&ny@nZK*%A;#?r4BP7o4bxbEQc zoDedP884ceApVGPT%Qt;#V1W7CVN7bhn7+V8pHr$QlI_0;@(EJ##pjvuBG(+4E+p| z)vhZ<;SM^h``BI*v|S(%uQ6W^G~FPq%2plK%#`J)4G-=1t~1VD)0q79pOf&I1Z1YNUK~N0Ks&NYnT~x9A58?k20`{);rsP}Q-5@;EuB6XJv(JL;&t ze*0|V4Iv4N-o!&s_H0gM2o{8r7#9}`f!IO0{^+L15{2?U!?;Y-yvdAMC>O4&lG^T?m77eo@WBIN)Ww#|%E*59`1+lH`c~C&cG^a+xacAtcjo&Q z+Cx`Q45G{$<#}ubGv$k3x8(9v@+33)Nle%>(3Tef);f>0qC=6KL~ZfTwB5;YO{71| z!siL8K3-dKl9XlI<8#8`Y9h^#t-2@RDOa*s7xjxQ-Y#`#<-d{adCx!ykUb0B0I!v< zZr}LNZcV37WCT6Rxr&2`5RjJE|0nam6Bw=|Kmuy_J8v71V2ge|I{AS_j z|JGQlo9sLJJCrE@g>_jl;WwwH9x_Y8uktKe^kXhPY#AnkV{qJdc6QoaN2L!wn9+$u z6c?|M?12CQuZdc6D36J!C_wC(e%|XK?r|gIAk;N^xXkR|`3hDo_6T^ssWVO*WJ5K> z>EB7~n6rPVB=u8eg!|cVlXF>~*-YC_LykT17+>>d0Tad}#yV#~3o0HSs-Ql;5?gkP zmg{yS)X4FE?bm1%?$PDhq=jgW-m3i`=g4K;`I{l##NjXrV5}Cxa>|r3yDxhj+EX&P zFh8hKE_@=ci2giNDcdTDHL+2=bOUXF1U3V`EMt>pG>9DDFTWWHH;LO{)JK1ql5}n> z18~RfrMxYfS$@B`YFp&`O$uGwUo9%uk3O$%{Ozja)!Y#e9I*wZIZ#(az6I#2a}E3D z-vB1xKaXbt#QUrLH;nfC&V{yaE>2J}RcBA`1hRNwhCm6Rg{gLONx3R0eV#|(8}*Tj z+cvHaI;r$xKLi=%h9{2if9$K!mE0kbBGk+9aW*+{R#Xx=BrYYy*u;5C7_g(l9=git zM-RpsGI@&sGK^F79GXStW_gL#h;fMh?(4GEtSGMjR-O3Z!OiHGP3_ob8=A{#({Oh+ zhdSbrSM_Jrnhm7i2u1c;vtT z%d(>WX?|RWz6Ut-GF2zdysVD|D^OJGD-<)rYDvDd>)bfhm~>~IpVbMCJ^_g34SMp) zEf`S=A5+BYtgR7z@9P_V9tdlK&@TnvPkTUs`4u)^cKn|FXIQ%7?P&GfrV0T8Wskc& zbBrdBKB?^bUB?N$)M=J9yJlk2`8Vj=zK$;l`wC| zaz3h_0$T*z&spfhYsMi_b{%|HleDZW<0E@gCd-PZPyTvoWF$WJq2MA6I>RIzGClW7Y_1pIM|)R@{DZm@Ol?Y=jl2+*f|7j zo-$hM(|uq)nbR+g^Ki66;!=m$@IKQ|mhq%s87s_4`fj!7?vRIimOfqsL|@Is^AZxw zI5e3$_SM-tMelY$YTH;{2O#{-dDAti`L6oQZN9!b{SF*^2v|3m=jgPx5qj~SALfXKp5s4_1h)R|}e!t59P5U6li zZ+l0ZdkVCx{~$fkM)pmjEW(6bWdWc;dmc^8t*YAQxH{$#?i^o4ix0&OK)obhdp3PZ zmCN=4rSA<3#-F#)5n8OOo4Xu;fzFG3vMSGjP$ zwvbHeegFEvihX{qNXU0I21ccRk2Dv*KkBw^!Jf7B;3Mb8%AH#Gwr?Z(T6q(>$i1!| zVAVv3m7db`fcV$3p^po{u6aOes}FX&-Y)M`Wfn_)vH89vhw-VW5DyQXjyq({S}(>z zstw%CSlKW-lEGRfHAntEuS!qZUe$}y#&s^JFef!n%=}9dYp8yuf5#XS!pVPVD=@is z=C_s?ijNgfFN_xS5>)AwP1CM>p;cY2;s76pjJWM~5rEM-H6(M)S?7R2!y;i+B$Ru4 z8Am_jko&6MO@o;ABddX<)CvfvVzTV#EnT3fkjbs~rOJ$8>?7x}|VZK1={#c?C@w>=YAh3)Wy zW2#uZ`+Y-3qyxx=N828(EXQ=skTC@oT}ds-OESkx=Rk;zsHLRKtQulxwUR=IcKfk( zU4t3kVZ_cmk&WabfYB?K?^4<=s<4yktz51_n`yfI>kle5Nc=dB+)S2^tyi zZfdQ~l!34V*wegh?P|p{gdZ4c0bqqS<%R7pYg))l3(Yo7pS%sX!=}%l&XU@@fLVJ| zaXPW*`u}+r;h^K0nOU5Rq4qR|i_E01$TbK&wM*QXu@F?`IHS*s&eK^;4fT>va;P5e zFEEBmaP6sv7e^fPkydzk)-KY-#g%`(om6y1%8Te*@9f=pPCR}UNex9a*)2O8WF%zz z!c@QZ25C?Y6#&&)Vz|_YGY!cl5L*uA1YB8h&J@$5$S|M&D#1iS)Fz0tVK{2`s_sIU z1X^Dd;u^CUMcXVq;nGv>?;L&=8ir|O9LB~$;~N2lM)k{mq2Sp1jgB1BIi=^kEv^B# zt3!=t0a0I(fs#z;igBNP9kh53f&mXTmeZ{L{Bn zZwf&JIVD<2x)P!zFX|aDINTP_E@`cecl_)uA|Uxvc1ApsUm&-0ng(uc$R*WJVH?f~^=#b!rQZ>ulGz=y(;dZoJk$ zP_Ez7CC^GO1Nm$O<$?Sod`1a+#7ZfxK|9v?0azjJgP9YzX5M=5Zr2(6nhm9t)0xnJ z4{1s#_~6F0DGO~qGcSj({kxjc+3>^Eo&5Jf`h8q1tq8W&+-aCDccVHV7~tA?-Pt=O z+FR5`=>!=^j~HV?S;(eJnykzvr-RUEtMKC8ni15d1hyqV+hJxHJ(_ab)Mh=eDdF?6 zW$^nvEp}9#D3ZER;P5e>Ya8ehz+Tw8-|k)59rhukbuTS2&zcsG21$G zvcQuuIs5I^xQHP2&?D5kIn-lhP?OR<45X5oz?gbqa?_Kh4VvIWJ9*Pw3&7&wDh*wi zK+76YA;Wpki?}=NrM#AW<91VgCmiNN{>;&Cw#yBeSd8cV`*fp(VJjU8;Ve%&s`b{{ zk?-2>-uzV}t9V>_LJsiwGGrOSY%CfiEGH#HlB{;7_i0ZP+kzT^sQD)A(Vw44J?(Y& zKC|cO&0@I-OyWmve?_wgXO$Lu!k`Lqt7U@vRaY?zj*aSNhjnp{95vmcv{bt=>5N)c zSh7at;QZhsdz#_KReXfkU+J$`{4OHd024c@wY2dn3=X8LXbW!(#-r44d-8F;SFQJl z6Snz|D?(Pd&Cym&06UpKxAcT*#wDuRMi>q}Jl2Bxe-s7Wm=@(kxc>B9^3$v3AGxle zIpI)~Y3wvUP#q{36M~s>*K7eh6Cya&>}MRaEhDVUWF(?9tORKve?OFCGQ>N31`IJ} z?Ww;G;j)%hR_-jWPna^CmHQfr_X&G@0 zCQ@{Fv5S2l`<|~`PE|K^1KhrX35z$Hqo8k&?0rQwF6!95Da_eW%vL_C5cRSG+Y1!& z^xpDt)-kYN|J+^!vz9%)yHw^Z>Gw=Cy<8@VHhy}ZWREGBuB;gg?(YU*L`}AkecDmc z+S|NiO>IbJ_TaDOBx`Gd8@*hq=3 z_yXzoQKm=%#4_0Zs+N&^HcCZF$B;}=dIA>XEecpwDzweliNY^zdDm_7;n`ZWlm-?S3pA{?LbZ6b%wAo>; z`kx#QTp=IEqPb*^Xl85j>cVcS-|#VejO_(isdGJ6D(w50Y3@tKCr z<))`XkYmlwgqDfH(X3C65+4pvZyWRq&r~<2jOb}4Oq+=H(MKV=zWubBJ~R8W8g z5K+&{jcp2y)r3QFMs693qH)S<_I7;Ig4k9GeYNTF)d|-T4>)J~j~Uj*wX z`W*`LXYmROlm7kx$X5Cv_4}Oy;co>&RR8?%hou^`lB&NM@1(xiWcY_9lM;^c)9+-` zbVX@<5YWo&P8Y^%Wx~o_Koy|kMVbI$3KN%!?dp?@w`60Jm?}h>4mzPzyrxR)a18h(WD7dQbtkRdsH+m&+ zXj~AX66UgL7_9sYy7$Crb@W_Vm(USk!Bpo?f><|P#XTFjSxk6$I%mr|VLR@GC{XC2 z8=JXqd37agqPuu^23^QK^zCX)UXl8Gz#pcz%F2JtkEB9wmZh*klbJZYPB|&pHA-E$ zK|w~F3`6jkfzRNZEk!1#ME}WA-_RvKPNF4VEYy66x9$B6p~p?ooMFK8>k$h|GH3`= zol!YxPa9iNHxilQ!%>qRVXkj7CbOvlXx5*8#UpsZp|;QM=YQ^AJk$Lf!fTojN_Fcn z+p*I)l!mq2j>=NE3UbQ>!}W-2-01HvoQB({Y-wdv_@gJW#s%RFGs!SYsKcg*<|2l^ z!hS`=GBDwYk8S(0O74)Lh)`iJmPH5jT*_3x`#TEdO1QX+3yP1{C$!h}tZ2`3*-$Mo zxyml#~$-BFc4aGr8}eHc6ixTk1b&ArO} zWHxGmG(XO@yJY>SpP(BwUbji07-VMSteTG*9T#!VOyeSfc{&e`ql#Wln&*-b{gM4% z#p!Uf!cB-f^|qm(S!J8;<*Ae=Y)vstjRbP7TT_%9l?%*7;;!W=Ldn*-GVf+K*E)sz zym?(_9@k033-+v2xys4}4{u=E|$8+2KKex(^3baj(%zUbDRKXTVud$Eu)_lOh`ma37R-Ox%6DG?7*tU`n&W% zo?NdV`?)|K23saYVzS302fRHPkc4l_-T!`K=kR)2F|(63qd-1dlo$Nc-d8~c9y>mi z>Ym|c_3FZ`y_ZsX#LHV1LpGhUFR5h*w??KQz^kaR&YibwpL)tR?wf1iTVdAO=&M{C zlV!fl_H-6PU&^K?S~mHah)~xt!w2G@TaVx_1@+$6AZtlnWQse(K*qNNlgR@pDZ^5D zlReTf0`t_A`(S#&vobktVmeIwXR#ge{!Bwj}mg9QO8Cf*f2Vh@|cfb?%gUnltuF z(M(^S0WE3rcy=!)~GI# zR4eF{kov{LtFEnVL+OBOIp`~zdP-@Vl(qQ@(m+H(61%wr_QXr-KewX)+@kI}S<}k3 zifURmCj8DgerjkTT2#Gk$8Dm$ifcp(Rr(L5k~hRshV2xPlE}Ki&`Gb7Hk`(Ep1X^6 z)6G|%*A3kx$jy~=LvZ!lPzIRKC{Hq%1I7w-xoO4IGZ4x}HVv7at|;&640(%Ju$%5n z@)>;pCZ)V!s*x?th|_TfwmITyE(I^YT&1X z_3qXTA{z?siZw9;pCbcfeM4{lT9liA+klFgHQ_aR`9vX50ziYIxqsQdErRX3`g2Qe zv*@+{FqR~psqCJ`EoHW(98xfn2cBG_(3t+&dWw3PiK98?O52ndYI)jNTX5%Xpt#>*a`&9a_s&DX3OceqKTo-rP zxRX?aXyK(P9fj?T@wKVjx}c3%!p8?1+l@qS_(DxN&4&Jiq5Wpm(WCUY@GOzS;QAlZ`$FH{hRgM=T&UABHos7AYzU%(#Ad9P7;YYdGu?-9# z%Epq+K8vN%DG)wtg?ukJZ~*C@ivI?_j1hNlo82P949d=fAI(se+1H*RHp`w`1ql*u zT$ruey-k=bcF%_rU470kv>Onl3eOd^jX#kXnyN=~`6*mVh-D_Xul&St;i z1a$B?#a6F)C9aG>%03CTr(_B1Q?u#Tn z*OlzADStzg>E0H?@UrZd=7-j);3~-lIj^7|tWffgQRSZDO<|o*4E|uknoKU1u*U*@&t9^$csJ}cONfAnmmLjgb36-P zXG=BusF}3FGQWOks__EZmBFx^$`=m;!(_WZ(il{WKhj+NX@{KgW(U6Nv3}wp>2}oF zGr~SIBsg|<&%i;&WyElYbfZA+zG7E5karaPMr^xG%;|u6L=UhB8bdp9SBfYO9e4k( zF1pdR{hFhi$m}BIbWMlXbYnuhMMbyQ#qJ3nKF*F40T5GR=9(YpAy*P-gYjqUujgt& zw&KL-{yb@OmZ1zo0&m96@&+uSFQNzT73XCTIb~Tv#;I>T(U&X<{q<=l7J3%MW0F4p z=aw;K*-lHa-98BDcaF7Lbp^VtMYQKx4u(oF%SDf)v?d3-9w{gE>9wET_7QrJ6HhjN z236BK=3(Bm_2|iXJo-JvIp-iplDM?B>5N##|dh*6Dj7K~IQh6v? z`_6$`%o|LUgy5F9X)WPuU@d7BJ4FfX2qi5qRxBjm(>D-Qrj_oht6Lv4x|$VN>*z2Q zeMp>?wpzi~cuZpg@?l5YTt1g4T$kU5WRsB94j+*ze2Ujf31K(0Qc5UHFzL->V^H(K zCDqKtn4mI<;Kn`glsq=5t}(0Tq8|+=B{AWr$<@x@v*JMVi!K4>TDU8-Gwl5Q$&vv@ z5$iEBS={ZN(WK{sK$rB+H!ybrJ29Vq9w46i#B>L9xjB+C-wtH6V`tmXGuD4;8VC5` za*Q1lz#>@1Z&RbqtxLahLC|W2%c18MsZ7tqe`BQ)MZdqLN~!demnH9CF=!V{R|18e4V4tWb%oefzp7GOwn5wPg-|v3H6Gy ztWhh09R+K}s$#7raIthPUixmS{xlO0jhoz#TH5ED)V($%_rmx~r+l9~WfeT=S&`Ke z#IH8@#^S>&3A$FxQWtGq$9XfgK1?p6HZI$!%FK4Ti9R>0Ja`CBj8K%C>L1Ult>4`- zM0_gY^r`Q+1zg4mRx`1w=LRJ*YZmp)7S|nqK|e@xj@A^#J!&z?b;xqtfY$AT+(PiY z!C>2RTvIVlpdE9{Zo8jap|LxodKi1Zt5Ea|cSOE;K4I5w@OF{4q$XTgdkZvcxHFEO z!rUwuxd5^z!4^}P7wDwh8yh)2?cNm zf#JJS#+KH1zTegP2P{zb*PUN?e%G_Rv4KXqbsdLW_PAd0pOpabq2|CC$arGyTIRn9ib5_1uvaS)eC1 z^K5*Qxrcf%DH6S0BD(H*eg2lGT@LqvoMkD^LPkNlVsl2WR0L5vrb+IWt1SzU}I6-6>%v& zb|THqkI%E5B0J9AS4sp!X3L|1OI4fVRcRO*9wP^>yO># zaUL}*(TU@gJoZDw4%VGlv7!fzC1TVvTP^O&t1D7&a#a?;9dd+EPWPmqyb`?MX(>rb zDfx~S^nRl{jNDYrN-RXA>A|*dR?;q+jx0^ePQYUQcgoPEyn4xoDdZT}m4_$66<{PLvDZO$!Q~qoME^QoLwF={Ah+hLA%6_78 zWlQR;@7_vnd9)_AG$XEvNld(?9HQy@1^1B0NS&=%8J~WkU~E50!%IU>Z^{BsgEY6% z?xH{@wv_)O{l*IK_z5OiQp5?MbYYf~qMkhqv0<($#COvg-9xwiTEiKvQFGCRz~~-U zr8`&{M&n^2Hewd+H{n?!)9jMB*FU~4yEwi zwQFF5ONYGXkS>DgR)rJh!HN(04(@ppQ{6=tHHLiJV%+qQt+`m!lcu@mJ==FTT&K5- zUF%gUIQa-PgOL}yWcA?4xs@cB4olf8x060<(l-R zk_qCph5h*Q0MA`i%5~e~`mUkx8dtgNnH>r_tyPd&oW#kEuHG+oTN3Rh1Yd$lpeATUd3ZUphyL00e$qH@Mjp?A6c zsB53tF6#&qo#wsFXri-bg!xJnPsV#rnKb5pUOpzSf_8=7tAJbW@}A zwBsTV3mn%90jAelJp9puhFM_?=QL`s560J87}#oQS4JwP!%;7C%W^&?u)n?3?IfvbzSsdII3@ zN?0d!eM|*M-C!3;Q#GkAxoSNVd-hGw-&%!V^mB>kR}Ne3Supu6EqaZ__)X5Ks43DT>_;yyiRSp76#6laqrYd-woK%bCPV<;A*$S41h~na6kL($CQ$Ga z{N*gmqog+N!wwd8E=}r^rWPS^CcX#FNs`GJsS1ja^WRh1Fp@;AS2F$sxGehg9qOwc z5UL0^B?Za`9DZ(N71b}>cwo2pU?<6)P|gXKvg&PfTB=lJ5an{}DoD)*dJg;R3vEqD z9^14yn|p%8&03Gn6WPXvxZ)P7%`TMElw*Vg%hFWQUc;Qm1%~Esz!0FDdJgBF1#>;x z(l>v-+pQVs05#gJ^KL528w}W12@IKj`Q6_;qu816<@>jrFo`gYC?H>-&=3s`Ws>Fe znrS$AFs&QAlK_p-|x+XGRhro81o>3P$*HAYDaW^rUpE~k~xgW>RWYeWx@5fA!_up z!*?`g)Xp6iNE39w89F(UrBsqT>nF~RpE_W@c#31D8P+P}Jl~O*>5P{BOTs?7Qh~rU zPB7$+;3rWI@%(b+8wYLqXj=5hFXhIa&)xUV{WJf?vI#!=;0KR?_pgeUo6wxRM;e{Ox=9p7)UW$dlz?=5!EzJW$Q&mutF9bTdu4L8#`%rvoGNWQMmBJPdmFmn6kJ;f-f(&c} z>blMr4!MeVpqxDT6LA_Ue5y>A7kXXLluFOV%v;A<^pfcbY|)xaa0?ZbGJZB;+r9o? z)z8yWbT)=#bP?a%@d(is5H8Umo9oFR|vvDgkCX+Etlz2i3=aHJv0fefw z>X#$NM5hIC;-6baZ};XE*gE_f#zWcWJfX3#hlr$7YaIgDc+Z}ygF)1gGi=Th+w9{O z2ltiOE(Rh`!p!qli#|oaB0hv0M?t}FbCkkBKB1>i!BeC$ceZu!a>2vVdh4LL(s9Q@ zpyAb}KVueK?E~}WOfBDmV&lh!|D~h>i;m-bwZ=xu_D^U%lG9BPT^o|%KKg=~fMMjv z6|99P`)lU_>|`0rfxrAVg>#kz%oD3BNZuGz4@)(B-f;HX!*vn zVv+x%viQ1lKws6OQt+-3fz8WyxNn`hgQ97`37l|FR{UE$8+Ah;`iP<0H|C1a$*$jh z?s4*}OSUECO*g4ArJGPXOc}s8T6Kz56#k=lKYrEqnQpVdoh_Z%X``1=t&r)-NSDkR z_=X{O;;-1o@bX1>4ULY1Ofh2bcC--`Xj{?@%)EM{fwAZe7cPlM{3`53R?4`byVie~ zD}u=}bl{FAUVeg6s#0bJCXp#NvXnFn>Ex%=))Wb!vpTU3^baL;5k(j1jBkSfe{lfP<@)m9{8SoTfm%JfR%6T5}b_ z0h)(7;Q0UCN@9clK8GmP+n|^yv?+_3ix;`>Lt}}Nll>lW-v0|upi4YGcVvFTnAg-U zIk=v2?CXn!ByH&%Ul}&<{Pyrb_L%g54TPSHhU>Zz`;?J6LF>mYT0PL<|6yGB|8co; z2ft!@>!GCTP`9x7-d!q&f2QskOd8+)rv`k>uI5h8;y<@;{l_M0_FW!X_MfZ2EbiT= z1c=40y6i2&Kv_idz%q2V*OEDz4`r`;vic6LJQFqM_92Y zgu!I@zr_;;`nm`p*jRUmoFls8q8~@I{i6Wn#HE<^`^5lN=9WT|PwLf6 zL+T0MqAbCWCJ506SSf8up!mt;$kkQfXi>J0#i)FMzfMjY?C-px9M{Xl`#;JTY^FnR z6eHk~3o2M=i9>Cr3o#?J@D?~-;>?8Ich>%H z9%i~OF!(DPT58fV`<&8_n)Jc|hBs6nH5K2q$SxK;Kt4H0yr$mYCZ{9~e;Xbs)7epc zblw+m@~L9TgM<~+iyj-TsK6>^n>@(o;Bv3EP0jqd< zsu+V#dai=muZpr%Trq3$kMnv|U3{^})4bRS?ZKd&0iDd+zuMx9w+^*0@5gSEtFXV- zEjUzbpel}Ro=rP-b~@a7MnWKn>q*`v6$eX}zO+G61-#^;PO*_oDQgKpF}XIqCW-Bs z`365clvVcTin^9!seSe57PyjlHzoLOPSqCD3a)1H+kL{3-wT{=o^Uig6}gm)TSae) zE48++R?gP9CIBcYK;g3zG9;dq3ZU}eFO29EHdK6zmFwlL9;d(4@JqUmM=%fp*0Ou^ zUq-@v60P_G{iggB8K)pofbp>*5|w=|+bT>0TGfmA$v@E4Ogagr?op)!AJpt^>M3|Z z#ZIP}1BFV_9|{O#0!)Z+-RN_keo%pq({xa?Dk!@s51%2gD1_MSn^)E|F{WUG$kJhd zZhgh2uHv)_mS(*%Lgfj`r;<2DO^YE{Tf^ojF8%G@Cyk~9#CoeL(3X|28qy?3^Yj-& zE$x7%hEF-@?QZ@N8(jaN7P`&XM5vmYp|^uh6_bOo+*LtmWOsleOKEPDDxO1HuvrYK zRH8MbJ98jI)@$dhIOBEXb^aQB_=N49*o^ww@8@56B^ohIO#aO9gJ;AHBfg;x!>3t2 z!{4~cfiEtC9_!!kES@*dTWZ(_+$JirB@1-XBHX!;e@lkG5J4BG_xOyZr|d+=8)(x8 zUooLSa6M$e@&Y^#wN4!$rKJ%8^9BwRW|9Tt>l`t?c}CCZzv~9v6TU~x{a=0ym*2?I z+;?Yk&56@~7ZLu#HQSM8T$1_l5cL;@szj~a9ET~HND6u5Wq_+O)HEDTy-f9+}yVJ z8lQ3A8XqYe{6_hG0hD8F{&K-CZ(g(E^Lad1w)VnEQ~RtFD_zH()mht*)`we}Ni3cx z6~Vovsz9dGMZ#ML#YJxrM`HC;XH|=jm-$gQcS57P(_XKF_o<%+Nl9r2pi6l732So4 zBcHwQXx9Hn@?=?H@<11kw=d^pbbRbqOQ9f1^URr0GDcI-*7nPXIf`ToNHa0jYx9x3 zLG>#^7lW#p(S?1K-O2b#uv+Ju?uN_+;&~b;YlKRGvqk(6w%W9^5ut2hR7}^35VyGG z9QJL&3p5>oFXK7X{@hYqW`T2B(ja5vw6(|apkU1k7<+hXrHh*t>K)AY%^;dIs$OEl z+{`#b?vaR-{N$%Y(B;MQbQ(KeG?H9j`CH*9++@o5cP=CwnJspUThe6DQ}nIVCML7) zHzhH@+NCW?$}D&KJ@(d1maO9gSD%VY7}5I|$SKeN9#Kn9bhY(8hI^R9>XNO6^Z-gZ z#rQ09KM5w@i~9Y*)0cO>Vgib$L}vK0^SxK!^7EqncV;Ee>Fhzn1!!^C z07ZLq<-7CAo!{KKGk48ovSqFP$DY}1z3X|O_jyF}ufY$PjWp6iL+rL5(HPq8%>;?+ zsly}(Ae*Y>r>MVP;Oo1#Jg!?^CIk4d)Pl2?#> zJ=y%7U#Xe#{Dcrol=#j4OmxV%p3l|Qdj{b%?KR#5iZ4^?^zVJdV!+lY@x59a@DUn< zm*6oeSBM2F|A-c>yRGyoW&hi-q=pNi1<|EWbh=9IJA3P6zAO&j z(p<0af1cna5FLs@>D3bdGKh|IEp+7*Ce>Kt|7C^arJ>Bi@;ax(-Oum*8vVOWa zuS#6jI*W4IJ3n5&qAUJw6@gs%DQP;_IP+&YPm+G7-5t7RO244W3yGLB5M6Dd??n%m zxhcQ;O#3Vv47Nz1`NR_@5_vl-Y({4t;m*#a zF!71BX|#Nzm@XrqF*okK8KJ9g|FIlzsWw+#5rvF=3*Bed--gM6g1skzy9(0_RhiJe z@-g3uv@zPGbPgNNSn-Xbjn9wFc`7kIR%qI1Z=4WBz9NJ5NoFUU z4;x^n?(4gTS>VSh^Lk*&_m=?@GkJbu`c6%4-BaZ@fM`P9B;_(Mrl=D*3aW&Z2|H1yak6+ zV|U3_c&icA)Mo&Se*_9i#Yyjg)FfTa@q?>Jc^001O#?qx+Q45*2ro9p(RD>r25daO z`p$OMCo?3b5q8tty0|B4!@N4aJ`h7H;N2H&)x5j$Ttn1swd(6Ytnyi7?N&I;o1xrp zO0$*9B4Y7mtbwqBNFFf*(x1S9Age4x`U02UOlAi?N=g?tvskK5JDfDnDFwsBc}Mz+ zi8T&&36I34Mvg@!2cREJ;#_^HkN$erlT=>6hred(;2%k%eXDiJ-?g6Gn`PK4A~Szo z7}~I$5nnT{4>u##d&VOJfh|Ag6~o+?aZdU+UlTc<2E6}Zoe-C}VOWy7N)E+@eNeT^ z$SF`>yb0I)YWHddql(C4*eOU7EaO|uNy)ts+kc=Q=ZH8vZ6j>CIS&CSK^NRyQyJ2l4kA(!Mo@atPYO~?)N&5>W6&uh7u=MPsK z2#vb|UoNwX=Hj+wUCOT;%%nK>zI#*(mICnMer=*F>U}*APZ{O}78ENQbO|dDqFgBy zXtIhM!i@8|B*0qj1<3ErPvc>x#MkSRi**N=ZOkdnG#1CnBL-&&yL63Bs}53S41j# z%xP>b@Bc;eEN;&x1P z6GzKrez((yqeQi`E|J)PoA6OEdYT!~%^Zu!H9z3r@`~0eK4%a4%4z>B&%#=e!B7=v zGLgC-U?07@FWy`egSQ#|g!(?$XJ#bT6A&b*+hl`FN^OgYEjXnLP`XizDft%l$@|XhyAqas8|~rFU?tLg zKJmtI`>tkD8(=-<*j&$~Xk^=D=AC;D6?S)*$)!OJDJ+%yL6qPpojo!1Ls!mae03ZJ z-r7vbNff_&e5NrZNtCJc5_&?2Xc(J+s5Iq2N7=cbN38oH?aq@Wc`*K>#eV4n*G->u zStjM!K1+8?_SO4J@^vkwwhTY|!*su+3u@_JqWcF-+U7H}Gb5iLjZe9x1{+8P@>jfQ z=5}?tk|k|Xy1~RmPB9}~w$u|5#cZs~9owN*4dw8_uhlsv`9`|WpTHbWi-RTYpFWZ- zH(Nud6%OX;D!C1hEerl(l^DRS6ck>6s4}!>Suj_}~^qC!o698^1;@~y#+sl-l z?1}ooILYhXs|Ux+U(%UF0cRU8>855J0#aNOB=bc*t{zDBDW{e|Ky}Ljf@mM8-o8>Y z!??|)yWNA*EQNxyjjE>ceAb{#t!tP2r-YFz)RikM?gs-|#hAT-KQ~qSE*TkY_}?4i z{mYf{zudeNbtoW0Yx2+CyHEi#%70$MY*rIe&d5m1^uKDsck{oKnUNfc0AvO*mZw`+ zBnX0RX~u|usgokXmyD#V^>1yTjKp$%O8cVIg3aN34{F)CX-n$USvZZgKF7&-7;V7i zabscCjmUmkkNG`~un=+BlDq38*1qe8Tiw^tbYd2di8qnYmhBhKpd^yO(M>)!bi&&a znnNH-TptZCB#0Kf35=lC9X4RXCw7V;ONM(Z4)&%m3fxm+Xf$1svpf*pl*D9l|$c9SLX`)K<5HT*t{gYZTsU9 z#Vs1^l+dv>s4dNlfHC|kC|0%j(Mu_L%e4ULfOdaR3ZwKl&558md+YxDD*cr7Wj4nq zm)UlyjKzZ`EQ9?AW7#D!Es+WMz_-Je%ga3D6Ojhrfo>#~g$et7F7D}{4 zf_kgQSDmc5^!Y4WVNP+Dr!YWppOhZ?MOJ>(vn;+fOL2i_C_J-s#nz0%(x0imT8dvbg|%XW|Ky7at>|?! z(3r>aPTh>v%u+o_>5V7!Ku?4rgXIm${Ga1n)PsO-Y zMKs5kI^d}mcLMA_YP10$vT4|1f%)~@NKk)8 zm&dM!Dvz3eLd!JPhdLz}Pe9tu>6$|ev7X!sHPEC0Gw=DnZ;eu2#%;ooQj_gRhG>Do zvEs?e@hg`gu8v0o?R^S!VAiAUw>#pcMa7nsC%#MkzX{g&d%J%6HV|`36BH92^zonX z-i;3%lefL)w8V{+lCh0SE%etByIYtd;{tsquUwmBJ{5|sqmm}^OPob)$;c%b0?j%M z(5gQYB-+-sd38Xg@8qnx!&3T2GM)ApW{X;cb(I7?bQ(z3hv}&vyYjWKt(&3(g?2{t zic9*Cm4i&go7M+ck!#m$XpZ zC)s>f-PIIVm(P%NlT%}>iy0Z=>4IJdB(;%nL~1&YpL1=%wbESOteae2{k&+&E46<& zY_=R>JlBGIWY&W2V7^<;ESGtN-_$d4O+WwEjTjNG+M~Ivx>foue}0-dQ<5_c=|$6i zKccM%WYQY=?%)9X#I5^O+r@^@5{_Q2~NGGG<)jlGjMmymAUbt|YsR zX8U+r6A(WI$NFwYY#sDZ!d`VlQKc=D4zHiKmrU|vTr+~D@`5;e3`#U6=7^411g6&` ztZ^I8KbCfEJTSK6_LK-W(}UTRh1GpZ0!e^=ku~YPI2P<~F_-J?s}a1Zsv{2;z~X0AxguFZ>hX#@Z9p-=o!Fmxs_b zk|S?kB>tT(fkpH9y?Xd*rO#Z1XWskIvBGr6K1Fg+k!Qf)zX$x5$U?^Q^!KO#w!;1Y z{J*B4{B4(_gg@%!CEjo>fK05a;`KiSe2Np|DQ z`nyP80=ZIy|Mb2ROK^hp5AUzzTVr7Z>P$R;31)^-PIAX6=ap4I zjSf(G*L2wX&|PtoZWgO%AIV^;WlDB!0z%ALEz9I36 z+tuo%?!X$bHfF1ux%iuLolZ2^2td+_Q3KFxGq$YytFOA+NW=$F6mzOLp0Ivb5 z^XQwHRQe4T{=_qq^E0PrY#7nx^r{sZ0|yh z)-ST8!Itt^T2fQ;bFtFo=e`DI4u5F8+(`(@D)wl>Y`H zt0{=z)=Bwmz|BaNppd(z|GvCYlLz`6e?1k9&@EX2yQQn>GJgeLSX>kLW+%c1is>ew zxbM_PO6*dw#Odc0&S^|8nZ)bX*>t-k)?NIh)iXfu0mt(45_?)j)^`U2pxI%&bxk!c zk?of=r?0?5NwUY#5AIWXCaSIRZHSDNIhrd+Z^6sx@)ibyhZpfvQfvWt}G4I5>Tj%2SvQCnQ^AM z<%lS(2!>X2eD^mWJz1~!x%Ib5m!Lhu2iY&}Yg^miGCiuy{g1d_|aR z|IZA`JDElLf)uWrwR>&URHg%if+R)H=@?MFP01FW_$dpQ9xPxpNI7O&m`=JY?|OEn zvK#rv;KR1Ydn_3GIxmF{qNluV&Wlyx*(LH@o&#R0e57qwU6Z`gC>_&YfXj1pb5Y+L z&hKqHzI!KaMZsP@iTTRiTAy6!XQ=V_(XFY_Jhq%7?jFF-Jn)dqo+RMUmyzmbekDR) zx3u=7KVauVo+B)%sSe*?6+Ad-H&;>FX3lz#UCFl16QQKc$d@f6z#L+p>&Gr3YJk<=~P z#J2#B(Y+Up!>>R41aT*x#Sv5WY1f(}9q96RW7iJ<%KduhTIWZC?F$wO3$P2U!AbNF zm|gX-rzwsWEc~$n&cY@IZon;es5!kk1uR?rWg*iNtKpAmsC+IWb7^RG|5k(2^&~9Q zdz?>mfZp6FsmUMy#*Al61yL=ICBO3hICX9A!CZ`cio)EFKIQOGI#7LBH#lb#BuK@}7RuS$n567BxG#GIx1kj5R!t2jxY=pcSL6CEq-<=3qRQtqiB8%2?C-?$ zMcUCmq`kSf_|4x(e^PUH>bK)kDUyz}Ut0=+ggb8Yd6usnD_L>QYHso{nTw>146a!} zQ7%X@_jEvvUgMrtB-mChHqq=!r5A}de#B{PKvTJm92T4TYlaY$Kd?G7ffQg6D98yz@=TWQzb)-yT5LQsmXLcv!u$vG-E zS4gFQw81@#yDu{qt?D$8>OQpuMT#3%77n%%RYmQt_>|rS7z&K86&Ywh>*vdT*CxxN zk;t{yT#-&~=9l3~DUJY%EHrT6LYp3L_R*E5){d0muhVdyamfktW$s1t`)7vhnlC;s z`PhW6c3#I*JJQWGVmJM1uym5t&N%vRe<~FOrx-dE$<*s+e9-T}=2re~mU{CS+0~)q zU_lt*(yqK4@2D+kn5qwJ;xhV42z$zryZYiK>;KEL`&-++K7G}U znLB&CckkB;{rgAU>oddd8g$J37ThPTI!qGIdvUi@KtuJ@zwh&3j_(o4?h#oyfxo?P zdCEOg7cu$00bm}fN=4$pR<7FGL*30zl<+6}btmPwkPf`<0uAI$95o)Q+$t#VF z*U&Q{`sEYUb}kFICcW&yIk$+KhjTyDT~%CW3_M1-awah&E1ti8@Ff1zvcIBqvH|}S zX0^pg$E2V-bh)pvKkktlF}#2XTh&_Zm`1X6ljA^yC6NdfM|i$bIP1!CzQNDgAtraf)t+`K)xP z`}HFkVNIL*pYO~^c@&d+ARwrry7_F-*XR{j{NJneoS*tjyPaINZD?Zag-Mn|t-I>G z9|v{_r-fry$Sn|`ujFpog0y?(amDk! zIhQzak_?Vv6qb~``@*T4=H=BRSwhcP4?+}$tc3AE)l#qCd(j7Dzx z1m+ew0}2Nd_m))E!5QP!<6@xkAf8%%29I5-;Ag8w!h*|k49$WbHq~_Xvb!d8;?_2^ zn=$@PdQ$Va+hKpKrt3O0FCaYK?_TKBYmV2NUi?_8i??=3tTw0Y>z;S=9u_bzMhaqF z3+&rNX=38OpMLiv2INcm8-_8u_Dp>yVj!Rd#x5}{246-?Upk$rlku%nlORJiU!26o zXJnc#!vskB%8#$kp`+=C6k{83cZ=;ZTRw{m^S2MbX@f3*;b5GGx}ZiO9ZeR&*gp$Q zc`7hCNYdxqA;?HlLX&QwrP~FcVGYGJi4vlRe=*Sx+`cFq6A4h`>MJsbPh)h{c`~d? zlM@G`(D;HV)|F6e!1PE9JmEkhcDKm^a9E>7R znvA`37gu+VUnaM-<{ldQ37+VuFEet_S)LwMfSi~mWB$bP$> zB&@5J>=-r1ia0Hg!GBH}S+!}6U3(U@C(WZ3pBW8|vUda(HrHPvxzdJe253O4?Wlb? z9S;hMrba>4?O{nfYMT>3?F@Cy@u-U&V?R`1R7DW-a~B1TyNFt+Yg5rGV+a8OKEOJj zDOij*@mg7A>2TJks{3Zf4oAe)1%*vsE4tokfXkq-uhEKMnDjYoHIVYUBez|D&*-GU z*2;9Xa_anRgKRvD?F!4U?kn4$02;@rN8!vrWGBr|S5s$OM1U*wG<91cW3PuWLs2OU z;55)4pXD#IoE5EEnRAA93(2eZwT?>kE4EhyD0k63VUa7I1GNu1=kJAQp?9fpd@GV+ z;M-0$$hoMH%x$vcSr=W^qGU;}{o>>haOJs|jt(a~vsH>+He zGtFwEQ)xq8T^ltMb3{+twaRYYqUo0RwB54=J??}pESq{Qm-0=I*sh`QzH`Kz_OHw&w+je>jDM(l%W1d*UT-Ila;;&Sz+047w@>xF00OV#fz~Y5TUgk zqf_l`*jPo}a5KbIx>gNi8*wfrl(Susx53I&1{dd6UisE|ksRfUmnOMpnm#bE9|5YT zWR7OU;8)cf%3Pyz6EKkJk~Q*a3=k4$`(QaVT+bmXnfNUt-=Ji`G*jsE5^7EIez`SR zyx%vw{He%NhWYvn_otDYBJj8PED>o=TgC8Xn1-~2F26PsSg~-rs-eiM?I7gwFq6iH zl`BEX{n@t<`i`mX>sEhUy6ADuuiZpjH}cEb$Z3@No`iUSJ>(iUfBKSM{-b=Z?E#k? zLuvJ(m{t~%tmdgBxX`o-22PD%Cg!1o27f{vg24#aU&ngyF{P7Cj*7?#S3=)9qlzP`aaPT`8o5rDm=5?Ero*Yk;l%G8rrh>FSjmHG>Z4M|jsLw|A-aHXn#upVq_aJngI5?(Y~x@$A@s98nYMo;VkhSz~J$ zoq}Czk7IYuXD(mOGS}k@9Zazi-&~#Oo*d^SJaI&rtMJdycofbk72>=B{{N2v~ea-#v zdqe=V&YCOQl=`8RXi3XkMt954A(#$=N#)k;W_v*FpE=qdy1&Sl1@LJ0?VFF0or)68 zC%+>NoysdBp95qYVGPeRg6AK5h;R4AS)xWoV$4OZOX9vRFPKv==RE=i!NQ`08V~t= z*nY6_Ej608dMp4-1Gzzp5j^>0(fw3gE=LaJ+C{kMjjNp)LKWr2X@*zIGQBmAb=8TV zYn*fH2Ky7bRQmKJZV9ixL8KPfDD}=6yC!#cjzxhE6rtDR*c>b0^&wYxZ4;i=HZ
    =PI0C;3-(ZKKM}hXW;l| zrQJACzk%R)f6K1%Ngb-MePv?f9;~Bq&2I`ykXLG9;&G+5-8w~uR4w5*8xP->>6v@% zlUOBob9rX{jmyDdz+u({$H{Ub%Un4A7W<;02_=KSX=7{QINFs4v?4B~y3UPP1bylu zv2bn`j3r7rm{Y$1qn(aQ2}%MRlh#56PZM=!Mzx3Mnc>Ee;brewm=OULeBY<<%eAzS zk4k$AfE9_hnqu{J z9^2X!HO!ZG2JLj9*j4!7ZWx62MI<$|8Wr)viOsro8tUo}1W0tSoAPoC{Y}vcd5J|b z6eRocQM&znbGNpSpr2DceXG(c>7f$d*kr(caGY+= zHe*T8YxyPpa|tdQpS)M5v-DUXt`Hb^V|O2dZB69zG&>YBBHCiWls{_ZFU1WogmL$@ z_F((`M&aC@%M`V4sXyaA+ho`xU;*MM1>B4WjN+Lxcg0&GMEAs?A9oVcv=gUGr{7Oq z$8aQ^GAX*RVRQXztU~Wp)Tsx-g0-*MNzq4bwlv6GMh8bisvl%!xE5>sY)g!yT7o{- zTlxre)npK?>ry=w*>c*E-Ib|%d&>`e6RJ%3HuRS{XMG-!_#BN4?H?bfJ2mdj)nt2N z>b80uis`oUG&`D(tFv`>%9uur44;rhf|IEQ&8Ezh(jA?i>4aB4kU(vZJuE+US}1LH zhKLA^e49O}`qbB8D&eLFwQi4oTgj`z{C%lfQ1YjKDg~QT+L56~a@9O6Ng!^xFD$#o zQ(9!j=(*w6(PDn7Q}dLIhJJ&?Rf1#qBN5L~_q!{rlbc>deA;rIedSsF)^~6gyZ$oc zsq+E1@#v@kpI_|6a1&d@dX6Oq?I+Ks$YLWcF@#LMwrc58RzJ5T- zrO`EY)C2>hv#QL*a>TRWKkfK3DK&AnqnheDNg(Lt%JF9+0^25 zUiE(F=4|of5ZAarqb9XCd@K!0FR1u+(6aVLuXfZ?XUvMm1;RGt?{98<>>Lb}^$+$P zd zO)zgyWA+xL)Jhwe4s{vRXNvEj5dAYRhEy;$_i{L!9-lKWwqi7QjeBAbg20s*@B5TC z??;t3d5ZY%?Q7Ib$4$b}XBQdk9esf^83%XA)7pwhH+oF#>!e|$=>ey2nOme0X4kk+ zLlye&>-!lzJegQ^{xi5J|4fXRkmBR|vO1iR0`rv{+7O{!=Cw``od{lNiTOnq`j(&1 zrZjYje*gVPC`=Rj0^&8H;jfYq!XZkb^!Du5S_3C^jf1(G-fh2H?0XiT<0`C%*$S#> zrcXQ*rwuw4|B@qB7!ln*7=U9&-U$67tE{uU4?{PFJUvI!IWbxaVm_dH~2B3 z$?720n^13DSWWk^83jtg_s5>ujnF<6xBc04#VkLt11Qi_97RReQ~OF>?b<3DuX&He z7aQ-E?R!`S2SD6}dthd>q)J`=A4w90yY2NQpc|q;Xdtey1zo5SIOM6|vnGF|o(&li zdX6$W$d}*@HO!O>w#>;++^3NYL8Pq(+8TE=1nE8_GA}nDVT(J~xs~FCI*~U9T;}C{ zuOD>@I_~gt5CTGBuk{QAc>D-XymoC8=e)W6LSnnRqBCcM2H;^Q7VobK=YZ7UuC8m9 z?G4?~w?ec=eI(a|;8fLsobZB?Yfl8=%DxyB7Vg?$lXov{a0q1L{+jj8tu5}2YtI@O z6<$zt?9XP`Lkvwl7J$ER>AHM1?%{g77Bf{-wAPy!ZBg$~xLCho-+)-hNp%D2Q{wDx z2g@+-pG}{@&`b%`3>^sJyyHyj1@EF{Pg@};2Z+h}Msa`1b&0Py2j$+?;=Do^hsAJH zY7YMs9e~%T#_u1SHZIJ<)a3)5r(s6OhHO2!uY?oY(pcs0%2x#coj@Wp6s4y}^2vrt zx&JuV^`KWTnX&+hJTo?Kq=j>j5)b-L<&aj{I#uN@p!5FX?|)QrW8nt)+E=GNCe!L zhtNf*>ePkuK7E-BN!e`+(LaJo+wz1ut}^6QC2*|0PjyRfnu%l!x_y4lB)ZICO)?D4 ze_?}y?g`0BAAmMvP7k>xKEi(Ny1G5L5Lrl%TFqLHJUH+|e^L55OJ#B5v^dy(d)LZX z@##%e0KnH=u*0CD@^^d0)RW_53w~35o|SaDr3>aE>obK_+}TryA7UkzT4 zSS$Y-SX^4@-D_zKV4|eS$jxx0IL1$^t3nME^*(SG*W3-cyupiuAN1e*k-&2k;et;% zcYNVYlT_H9Dz--RE%BtfkmK!wH&}mn@Vpw&Ox~Ucn+HFuiZ&-(?0iZ6f6FA{_~-Zi zzbXHIQ1^#9nUOLO_@Cgk{~h4{_k%lO&&PzwD7?b8{v)jZIiIidH!(0b{R@Vv-7vam&gzbfUs(BZydas~S|$ z-Rl_Fu(<>wg-0c*@mLzef4FEXed`QD_6B>#k4)=@$p|6V@x6935~E_?q{0BZ;x zEu$#M+KLQIqbY~YOm8{6+}f~-db*Dh8bAraq?9^|i`)>^<wr0)h!;SW@f!oLPKF@KV#}F-vqDj46d3{Q2<5 zXGA@^_eNrKPNpsH(W6@qqun^U=t};HY|oSMoeIH{8&wq^wcw4xUKfP$<7Au~0*rZb zZ$kS1Wl|z(X%7BUgoo8IB-mynPj+>u6U=KiORkuLjpWX}bDb?|KIIn}qS1xgZ=#(> z6TF#1CY!%#M3??g%1$2DPiP)7&7Ai^i*BZxYii^x*TJ>kEmI-z{@;t9+E z0In9Qr_TcMF$iz#J9i=pp5GTt?pu$GM0JOp(4J{cO}t!ps1Zdmb?1$ka~%xoz7ziMrTOL4wqu1!dGNnG>l`tz3tzLl{VD-wLNWn2%+L}Dn zG$g+Z>X7)+9fD~;qGFo|1TCM9 zYpT;X2(!@KiYo|UW|9mG*L(GkBNqSp(hq2=m>0R# z{4l&*jH%H0+e04%HFf`M++?GfGFmR$~6PhN3hA_SHiW z;?G!)=iTX`+%RUWyyV%OsFl+52~>PO*RkflR6{AE962dM(wSbi5vv)T)rXEV^zEY-#aC$WFgtN!s!pCr` zrtd(tqBJPWyN$ks1GQY(sh>rk+H@WlY5<+m>VDaa&z@^5%gLGpy=b?7N`pfKU zV_+OwN&WCm3UkxR-uF}E@^JVBx7R8tK7Ujo=FDHLLn4)@4ru9umUM9jIIRV2!WV)VE^Dx-&f3q zlpiiX!ZlFa{Fqp8FQx#-1-~nm(ZjZT#=O1vU86qQ5YdC0Zi%C**?!C|6st=+hJMD$ zqueL^cnMq(aYWjf>}(rx@|@k)}EMaUOflR_Q5_0r=t zNm_bdJJm5uGIKK{NyGZiVl`~?3E~f2xpFXqszYW+V<8BJ?JS3WuWNi|y8@@J0r>I&ebBj_?99smu~F5g~Sm}%o>iug)X zO9hF8Jgl37Q?0AgCdr)<{)ohn2GnYhQl$r`jSEMSq9yQ8eaSS1JN>RM2e{p<-H*}h zc7*1(WVgnbd|4H(1IG2vJEGaq8OMN%O`|To>A>5Max2e`)_q{|a2~X;lJm$6_#rjH z_?d7sNnE{;@aKYKzo6e2@S~B}`3SqaWvHv7=YF+*p^Ay#Jmz!01W-9fzY8jI2~1|5 ziw0%Yp&Nx9-O=hH3XDgB7rDgSn>nJw^3=AKF9#1?SyLy>kFSy@Q3_Z0wHYnFA~fea z;{?@9JY&(6wi%!ougIhYe{mXY%{72~mW-c?l|lSZX3T81Ch2q8*DQXKDG1&9Xhugf z_#Xn>e|d5hpGP11_rL7&pKtztNES>+&+$(6SSa`|W#66UV-fmPTL$-6|FdI$>q9B} zK@Z{16_bR25KXhPA9&q=(kw-{#@D;d8w~)qm<+f`MVaLF=bSXY1qZb^p?VOmZIN0M zmy>SxZ9G0)y+6qgjILC*mKiB+eSp13Rj(`HlUbRlK3dq<>`MahI;bC*UX<#v@Icd^ z=be3PoXAzOjQJ{RrZ*01Kua$_!U$=iRH`n?ds!+T@ygzq_SXAaBpAf80a+J}rzh0? zr+>1C@Uts($ik-T*~cyeQfcREoVS9eZQK6Yzu%g<+yu>M> z4Wjkk@+?g46TzBbvz~!um3ar@>x5&L8ZlhJ7KyE6_s0IfR=@_INk2JN zfM`hGL~GWL9`t+-=`rs99E;PVA;jr<&puw~fNHle%eOF*K%g(34Bh5z8$u5TSbcf3 z!f7G}G(9%O;Cs?Jqv)@#iUnfPa6>#jt$C^wl2vnpzi5O;0oxwbxr7n<2i> z5^_e44)00Iw}#Q~Zj5f#%t8E3t_z38aD(=4F?m^&U4)C{#GN=dutxAfhLYP3X}w`>+^ zKFyFgCAV7*n<-sq&2>Ev+}*~uujzJ`KOInJ(JdJ0>sa9=PTJ)MK)fiMXR?A2m7zCAGNuaqa*qC-I)WhU9Ssb#z&nVci;_AaGSJZDdR%`wXxPt}wQg0rW}o zgI84tl$>VQ_j!OcpH`|xwW|Bc5t?WW@Gzva=4*|&91JX95h%5H0&@;2`x z{W&l-(Zg=iBzQdwcXpz~;s_prg&^uY4<29xJB*5rx8m5^5aSE{`dl1iZqBoow_QV> zK8CTfKQfY)6|j>c0zK$pOYnRY*U?qn!%L*kbb&^H!w?w#5O7n$N|;OJQtwc3 z*TD4Q0Q37@3mNj~BA5G?0}ohC%a_xx{xWeQaXFj{DR%%_~r_neo@mmc^ptyJ}0O7 z&7p0_`ga60HO1ym**lZTGMH% zvP*9%nuc^)P4}UFNTK&;FXbj9Ny02 zuZOw8Gl!$wPsiN;j#UTFH!XKJXtLFY{=7Cf+KGMlQsPuc8$^uP&}{$0cIM=U7kYy_ zwaAA?R|{bFBu4AY;fgF@jC!1OYsK>n!I{pV`iL54O9|vK`Q0s1Z8NWxVdjx*Bttm>rFVY3OCmP@pK!PSFHLZ_+OR=Lb?Wp^rn6A=dT zsY=cU4T8$`KJz0cFF;MAnGLZGD;XB@&fz49=Xl6B3O;lD(hyW0nh=f0qaqUt8=^Bg|ShYB|e{uF0 zFhVjs3ie+I8tc^bC-_U=-6?NZ2zc}aNlQ@E>bdLn4U3*n3Z)iU#OCoVw_k~^*e*MH zT7i`7}FMm`%ecC>e z4Em=SRGbC6q2N|xOd{;`8%ZCn30|=WmKd&;PhjfSXdc)6+?9%@Z-if0b{RQCF`W0C zXF>i3uymHpi<$Z`%f>GzFzoZAN}DGW&o&DpMcoZL*B?H8Cmv{#=gLcCTFxmlKny_9 zVb6b&as36B>h+5hVlQQTmxv&@XOlI$ z9h*|alq*Jjp?b~&FkjtNo#&ZH7;!IM=OA?rReKRHPPG^uq_M@C>)E#V!eqzoP+PbWZ;`$ zZL~U;0$(q?HBz%ZNDMy}AX!d+({Q?>_inUGgyd4s?AI0WWQJnniEpQ+TXDBAgLxCR zxm_k^O|FC^=t+kIvE|9rS+{QV^Q3y;|~S`JuPEoV2;{-__7ju zf?-1Q3K{3>@r1`LIwrz5RA(UIZvH5`#%R=|N5M1lk7L$PurxDwz&Tlh5hUPq`Q&5j?4%oFC zj_)$?zUWI2H373iA`T%+OuIe%4X|9Xx49yy{(K$0wx-%cCf{vQwq6LU^papxr1We+ zkr5u`_=`*ktW=U@8DSsz=Vv2UCP%R4s92LXlP=~iQ!lv!;F|H!WVIAAVSbDD_)a`s z!GZN}XSppAR}5os(J;gP!5qBnv@Iw2wI~z&>Gue?VSoeicyYK~&M==h%EQt?$Ge~g zr8$GQ5jgS4MNIh#gY%~O04>G=5qu!0+qsC5YjRT`tE6`J6^M-z{l`d29GhSQm(YDw zcIvj$eOy)YXlP~nNF#pJH-9(+G<#v&gY=87J-DUwh$@TRFfz{s#kZ%qeu|ms&#guV zq{#+&O7&}0!o5+^PJGXmaTU4t(E-yl_DpxqyBOzeamHcV#s|#ro-Dpkf|v?hE&V?C zl~5Lr2P+x(R2qO6v*pm7wa^X{5*xtsrNt@Oi?d`0x^gZD?i=}`=V4K43+V70iWR%4 z*Zu&Ip>0ZN)-~V(^9V!(2|yN~=?=f8WZim%@bTF*8EKR#TImm4-L)8J?t80XWSe2E zb&}eG4hW%gq)#}nRs=k8TUln#=s9L_s?bhoPTS_Pj=fcgW{5Pti>QgU_&HBp!rYND zPZR_@J1OT%n0R=qyx0}Cj4_JKhQIkyJf8H!muIK{f%OL;BcLBOe#&BEeDm=OH^o|R zo-$Qw)dC|2%hG&sY@>5rKXHkqr#?7%9ar|>di(CMCc1UsSg-&VP(V;XdQFgClp@8#rZb?x2Et2NqDkm)#TEn zh`R9l2p$+*4RD#&;1cOFY;8@QpI+6Jm`~m#p>oPGC+Q#5yYJ<+@B7u6q=Y3$(68jv z97UE*aH-nQG=EIZm4J^eD&8HQJ1UJ8_#0u>|ib(7UGA{wnpMvgNS%#{9ISTFs9Z~>*Iii#KD2^#){5e zQSir-X+tn>7t1s1#-YKL26DWk)?BCS0DQK` zz5XG|^KzIL_8XIgg!lb&ovZI+biSv#DOW7T`XLpAyXBqd?+y!y7kRp>;zXIhdfME@ zz(3MD%5}}0wc1tdwZHDJ!4WOhqt2gdDt{oDpt1RlPN(g!2~(D5ui-~bV=NgW9$ebd z;WG4trR+bMx|?hXHtpR;>!G>UxuW*dKyX0xqbGBS2USV&$L|&I;c5fvQ%vDKi;6{w z@3*bQtX#N-O1_D!orZ=pW9t3FiOp?pKXe~j7`Pqp7k8NZ7cyann%JY~O7o^H+XcVw z@I-!-*n14duX$#B;tt)4xsUJiFU5DBxLVh)dxm1n3mShdBLj{S;@|Y-+-VX@{!~JW zb_Y*C%Xg=RI;&-)$7XCTmd@C|A6)V+seFQE3w@o}+nI}N!r*6YXEObn@s3701%Uth z2^K^6{+v6mC~WrmSa24jrF$YX{`g)N)ynN0x@#;H6MY-F7e?up{J85$7yBLT^EA}` zh>ouVVoSn~>{kS;4|R=WN4xCCQO>eo*KOip$G(zl zo)uqrG8^%S{!=oeU{%?}wySa9u`;LP+$fx7zoOtcUcC2l-9jb&(VDQOi}9DE5KcRQFl${tqeRB4=qZ|^Io=^WIM_k(FrzSOdBrgp zp1*4{LRJon9Tdc8PrP|IXGfjr<;HSiFteVH|3-4)@eTiLNBF zK*ZI+tYNtb`o>lDg5Y5xk^RQ5317c!;>$0FR}VOKu6<6}r>S8iNb@T>F%eS3duHUG z1b-|?{E}t-$lGE^;t*%)PYAG!&u=j;zaDAfxApW{{l!q~Dv3Std&#Vr4I-iWqBOU? zUIAwu>k>k5Y!{#+f=TIz?$qb`{yvplbwR}hrqP_Vbk~UX#tMYhxusaW#mYYMH{*-D zU!%bRIBfH$UUzkd)H(^ca4Fe67!k(Y>SaZ3TeVe(zT%5=)@0o|L{NEBR!i0A@U?HG ziu_Qv=6HA8d?nG-tEG^jofSr2sdbV`kE}`znE45au%5medR#9uui?O&P4MNR9}Ut4 zPY8nW>kEpk zCT}{CfvTQoR3QM&K~6W-Tc807x>+OY8qmDR7DS_|ir;Z?18SvZI|diCu&=l`Xi~*{ zWPp&p4!CLuDpkXqx}=MXneUTeDLgK7SeJ)aCwq+xf+nF_iTBRDY`YiFM2Th(<8IeD zZ1hDqi{*|^f7<2)GxQO{9U!7BNiTKIlqxWM4gQbGn?P>0Y)i}o_YApB4b-Rn@%`+i z>Vv@&tOyL{)VT5bK!BBjf4z7ih~EJpHM?$jB!u~ZJFhq+6D0|I$72l4U9A!J&GRY zW|yXx_d-*=Y{BB*ijnY-Mn3xaK!81*O=ZN1Bz!t1_pJzv(h9HC@Ka9mq(4T&AU~Z? zlr5$2se1#!6e9Jm9=&F)UypCWXCsjAL7#(;1+!&0Y59^<*0yrkX{>L+reechSsK-S zFM3Jcy_>7%sWv>^ypnT#}1g`A2tDL+xM%Dp?GrL z`OfHoSBqFf=(Cy}jvY9Z4jt}l|NB+(=&|H#5AT-iuY&Dn{o0&o59OAdGvP;q%jVA( z&YnAY#MD%d_U{X`2yFxyEYI90n;6R`T{9zjcN>IMGXY`a2cq(B8 z#U4{*`CuIA3yO^#rIdYevC&J@A}6z zEsgF4IUDE?t2Dg`hZ&p@mQ!8*QR&nHjs1h?g~JyuucfZi>MeTGW|QU_lP$K#&(L>2 zhBg9@>`rd@w|Hnt^%Fos2ss^N^D+Gu-Qo8;%Ip`{i8ccwKc3b zUND$sd3;sW=tO0!o7OVLX%5FyNf0Z*OsQ6yobIbk>-`R`s3Eb}!>aq4AV1fC-L~VY zahU$8Xo>&i>_E%{m+dCfV;IVlBDz}XM_%A9D^2yZMFjWb(CZ>c_~Qsur}^;m^sTyh zIz3M1Sn)PbXHE5*I2s_!=vqg_Zyw))5;V?2GMwoicND_tm7_RAe*m+no5P7@;%I91 z!Q`G;TdsbkshZ}pdicZl;FA01iFE$Pc8~`@`9_*C`{0Aok9`eiO;(HC_GqJS%0g*< zv_o)Y3zlzfENZ-K;41|=h<|xmAjLhtO?rMeE-~(n+3$_wV7s`YJCCFmQ4*H-Q;qqY zvC9#@a__~R2!=0>7V~T~PDg8JD!5^-bvjKgvZ?gBV9sr7dM0n8y~&cZgYFFD6^A0L~c?j|CPmPHj8fF)GL`}WIUsm43?5>Z?6g-vT=&IGRf(blng3?d>_~#+OFs87Oz*E1!_K+ z!)31ujYe=zi6(;^el~ZNr@O9)Zw%&-Q0IMG#u)}jESo1Z10cXqJ%Qq`j1>Ra(W)lD z$X^BYk2%liX5gY-{(@?ShCOVsc5aGGY(ASRuaq;nclurgW{iC6Qa}?@$j`lxQYIbqiGXIS$4?cR!G4Se`DQXmsI@jB!*E z@l?5gbkz}ZEF1An18kir1lY)C3`EZGbY1#~Nvy0+W`~m0qa4!_51FVX;%c}x^*7ai zRnol2U+0o3vl1M zHkgR6IcnLbosk+JJpc>un)i)xw~&OB_E=Dr5UfLuFZ=eQ2fxv}Z_1qtt=a+vcMnF+<6?`>CHuo?ct#q9SZTb~25x_1{0Ah|9gjvY5}v)*-PRU%BCXchQoSw##_z z+rc5TYi?Ow>haO7=9x@0lR`Y*FtVA{?VtS3uXg(=WWxK0+PG?10gLqVuC(l}yBMnh zW(lZL4|yUnvg`PUJj3wx{2Mw1-PaVPu}xI~yI~|#Lq70G)&^YDN%agg&C;=y$zKTC zv3d@N0u#3!`Aq4m+7nO;eZzsx%(PVP=yYUJ~nqe>P-RNhE&%8%<+l^asE z+pZYsSdTA=STN0fXj~vIApqfd@~X|?NsX14jfRbml|sFR^9b?wGBU2sAqA1SO`PipW)vw=38v0I!Ybpr7Gp(#`J?d^%W24FE2JpZ5LKFE#VqD|^}p z1vPpKE_E5=#2lGeElq1_O2)lJ;FVI`Mo}RM7^ol z+uE?Wps4Oun!3ELxmzocO5Ga99u*~s3(Yg)$^v&wnD}zYeSgx0vudzzJ5jJ1DSG7T z6K{4g+bBRhzeWW}W)%$Jkb!rPiF+brpM?IbpJD>IuGS!j0ol7w`Wl=UsG2UuiT4)_G}o9BlkK* z9|NA)ci$H*5h&eId>j;|uB9Cwh)!{-2(I2>Ybr+!1QwkYA0M!& zB6+yixr@l*eo-pRrq`2^@85I_Ab-{7Jbw~(Zt7m)$ffELJ|O`Ok%JANd(|agvTAvi z8l5C54;kCb7_Lr(%CVgXPb_Zspl!t2YDo@cj}U0AU9EIh7?M$EbTmdIJwSV+lYu77*qPm~)JFRc- z=H&{wp1s$Pak1Z=hKYOQo(|=2X?Ken>eZ(XWe;De4`HRql91M+_cUcjd5t|}|N3Ck zOLl$gw@hv9)kzE2OuXL=W@>MRY46VSO+TLE7>9Ph{>ISy$`SjW^oC$8wl6qtUk+{E zxob-#P82NkCx*h*0ly#w>*LB-?9DDjQ9;W<_48=Bl^%b_0mB7Llhq1YQgei4Cg__w zGtOkpBuffe?am^Pz3?UXTMlAbS}p9eCcK79Q_Hj;kYGN8Z1u48rG@^OFXm0#XQ)xe z%2k5fWG9UU_cy;dAO8x&&5Hevhx~!wzdAQhPO`nmI!SgL(WRN`8vbwmZdi-BX-s!` z`IMiD1`Q+aL zz1LG9Ujcq4%NVg2vm=|=5v*CYAxUFg3@%B;(nn#{#*ag+ERBWQjC-RRwvUrlyR0@F zG7^u(KJtRsjGN*MT*;Aay}Z4xYVTzu6pCHSHc1G;=*j2F6qb8J77#C1^&Rcf^6}x| z9lO&rfJskHe+#;LStJ*Fn@)5*^X~mWvkmWzbBt4R8d1Ofw+C$y=mI#@BlPY0wEK2h zdLrZJzo&%-S6zhug$Dlh&>owhwBtY8lWqJX0R1AAZX!%;mtUIqDGl z`l*t5aU^HABQt@}-|}2Q`dZ-X>$D0B0Qg2;+{{|z8;T+aocM<2wEd21oNBy(?2g3D z-Ad!J5FuW%9g1LYS&D9RI`OH^sda;%D3wuCT>~Sexu@88gKC8@xHSuQ_Avt z4&p~TE=(AvdNKaGpFHRV0g_Pxnobw|4&8ysf-7t)B-&Y=*?u=qZCop=T}@&X6-N6G zg>4PfE0&07y6MB^LfEPO)p_p?D?_zDVee@S!dPSq{%bJ+eBz`(H+4?~o8&Wz1wXH46V4@-WNN^rk?-eASx zW^-OveKBTu3rcIs_UyH1w~rQJD@|dEwchRSB8TUP6+^e6G$K0{$lrZ6o7~IqYi~&S zaPi4-Q2}w$4`-1E`+fvjdRsZ{*VB-|BQ8Bh{n+=)7QjZ}d7{7XcSamCEM+TIXHJS`du~C;{7I;yAYQu(hhk(J9#@%% zrb53#dy&uxRk**G$O$fbs$0@2G>-Qw)*yVB$las<*e}>d89f%^7uYN&V@nznEI}i5 zUF@*~Jt*$}d65b&dN>bJyEUFb~Md%^Whv=c(xWddIddgZ=xSV3upmUXMr`qsTP3*>n3L_5Hi@;UjD5ie)9h zyw=nwA6tf%Fc_-&5=3+EitQFqn3C3qkL zBrMuUdgR$g#*Vqu^LchYl4uIaV~W-5PGwBIB*wtiLUSp1z;R-u`Eo{ZaZ1qQY_Wf= z1y+`BwG-a`L^XU!Z850gb1I?s#Yp{TZG(m%LbMY$2#ysIin_p8Fj&WzobR4DNb))@ zz~J5t2X|j`+_B5jn&aPejf=CvB)gEx>jZ3u0;ZsEz`n^D;gTB`W(kg4B#x|6D`>Ki z{zGPJXCeDvuzNKy7RMP za>d*YsZ+fsHvDnV6A4pRV(j!n_4gbvoEo}4@sEcOkBpM+sfQ-f*3U;eYIT;0dkzzK zxwS1G^2PfQ4HJqv@_gAv=yycp9O6Erw7mLvk!~WgbS1(bDIXW`&hxk2YIMY#x{r>8 z(1&PDypQ|Rkk0VFa`<3N2L{O8E=Zzak9>lpJt+4^(7{4U`$eR9L~ z5xE?|Wc<$fy`yri4z{z`ikd*!wJpGedNMt_vOih`wTEuRqck`h0|*D>) z)p=UViVEBKbIqe%?8&>FW{Gwo+}#%f4At8!5L4-fh65d?F3fO*Q2n?r%;j+K6zyoM25~m@Y4cewV0?vB2BavAFwR=_kvV z&AEK!)$Gj)@t^VV4l=dMk?YF*V4dpKY_PKc`Ck0xO&&>|$;{jO@(hf#x1sx(Bf3fn z@2(BbcefbgFvj5~kAI8GyF4GR`L$HojZlZyNjyTxK3__{Q^Z^Kp>S~|tHizZ+LndM zSDrz=y%85&wx_7$j2W9ex9`o3oe~5i>$T>nQxUZ;I#S)cp468~7F}qjf^kiO^ubr0 z6`nk6a_>java&+HV{rn=VRN{n!ELCBu%MJ@Jkl$^*eb&;Uy>i)!|(XOjxiZvFt6gE z8#J`RyN}p+*$(Ww2%hXkDI=R5+~_-49E0crH|{Su+McY zR*jC|W5F4N>!PGU#+PRoRh7F|9<^QNbN?>7s%WBW>!kOxJORQR(en-==V}STfVKBU zg@e&%HkcwuRyH*~uo>%+g zFjc$yTgJWQ9nE8aAd5IIlHoXDCgrT;5VB(n!#cFMoR_gdXkFTAf~2_f2@Ve&-o?rD zZ*M(1W;slR8BfO0!H0EtLt1qMgj}nBu}G_^M(8%@1LmhH+63*yK)-ehWnsL*rqI|P zkEus$W7mPiWpU96{gB)1s(r4t5@AlXCN=%4D-kilF+*r}@?$QZ>Al(Z;0{!4-#(Yk zJ8*7a-Bs+PsUUOx7>0M5h?|xetytYxl}a%tDBk#$vh{aNxTttmT~Y)ZaK^|R7J$yr z)ZKap0*(Ju{$08Q0$l-t82?%G*VX_}CJ<~z(PUj#8}-)(@t=NBeV(S!w@l+%m~bNy z{Siz6$UVK(;6PLUHV-2nyLiN?Pip6 z8jaJw&yJuvXH>a^+x1Vse{{b8aNR97H8s>DIv*5=%;a_C5Z1fERVt=iR>kPld~vaD zzi>Kmo|vMTp%-|)@M>?Fn^qWh~cvOMTjkw-zow?qy(EP2le=u|E~Lc!2L6F%?fCqww#s69i0d5vFc7a1CF|-Y z#QTkNLfpDC>GiJiWN!@<8yW!P474Bj2#5*;@cDW8EWhDsz%qBz+WIU?%|Gl)$Kt5V zc6_uJDzS+h4^36dGj_>C{@Xqr8f(_~_JBB2aLEsA0V{PrZ+guDLu4LKJsDs|3vTKm z{G0-WU{?`;hEG3E*6cBP1y{O@Z$Zcyw`SVSh?pIKG07G59CW(KNP|mdjy_q^4TOfP99qYVvIjZ*TTc;6n?4Cyc*SvbT zF2pdmf_;BHf}b-XJgN}Jqp}yiR@)ZjWB{3~@~vwyl1Ga7K( zZ9)u>0_aBrtE&ZWS!c1}c6)Tog~<|2QEm0r8jT(LbcfYX3b>O|%9w<+8A8Z`U3){x ze42^5BW$VO8BGfvWG&lS?2eHpl^Eq=nW(TA*TMLZpeCEFq@$kJBjMT;zo6 zTNN%AEI#${(p)#!$re&nL;o7XNkKxB{17sL@(Ncv+WM}uZD_Juu}xZLYv1-+`s z%S-rWu%rBRc$a>s4JLM&G268JevRZyX^C}W^=U$ciJkFwPt?Vpf?vr)`0r9;kC3_r zkC9}AdY2kWWlKEQVA*oUuZ}tSf}MS~wFAhFw&@dqD_enx)jNXXG)Z3c)uRQ_ zC>zc^0j?3qk^;zE9Qh0cLPq~-?_qiix&UlF1F@XG#c%zw`WbKny#<{EcK`K4$!jED zMv?uo+&@@Mfs(Vqn{Mz@6$p6!?-c%6@B!O+u9l3a)jd)kPV14Tx(i_~f!IM_8w!!n zKVgg%HP`xH$J^r+-H0{Eb0s0ZMdOAR>k|eBW2tfd`HdTdk4Vu09@&6mC~@5Wnlr88 z(40q6OTgBnB2Qw&0+U%je%>H!gi*03Q0_xv-bpwEWM#cY^lQ|q=an#;_mEGUtI=x5 zH*>mvopdlTtID;o`zvBiM=tj~;4EC#2`7d(hG`9HZ1__YxWZuo7ny>6xqo(OoH1fe z*a4{^G1&YTMEwB-qJI02{t(XmyV33LfXnmZpsEW>KG!zH%SPWi{JH@Oj?&!<26q-Qf=QXdV8luA9Z_2V`XKFgM!mETUf$1B33Iq}@FFXH1m0tv4 zKK;jic0Tm~CL#HsT)?QZfzBDl z-0397$ul>Ga>n_42#I;!<`6q%zas`24J!{#CkEFDavc8P#de`NF;SbWk-p#XyYT|cya2IygTLA>_UMvd2k{s0=`wImwR zrY}E0=n;q=DyQfB0uHYwji0l;G)leZ3iU0V<#kQKhSMWyVeXcLts zgi4`gr|kacnbB*;__qA6@9+QXdasM;e$I2BbFb&VKj(O5HfrlAVo-QKnKvC@KJZ~6 zG(_9(&Zn%bq~%L>@Td7ga$2swG%vRRFL^$tjlMLe07ohynt8Z6D1mPwFASOt-q1(8 z(Y&=C{Hc(f_F60&i$i0vSTqSk!IKow=vCl9FwThP^y?w2s(e&$CwL9G>9sgIKo~wH zO(RIjnC9!{;5j{rz%mF$CZUP54c!I@hQypneR6Zp`s<(^3D?}s_ zR8=8Ae;~XUU(mVq&9>nUXyM@6%u2rtk(LTiMGvXBeA1K>tl!=)c2<2w&~WGau|;04PaZ#i@;uHN8}Z#q z#xFSGbb9dkio(LUqV0#gZ}-*rpC0Y&u)KQjQR}0=;i$Jiw)g9M=OmPMwX7)Z3>?*3 zMHJG>iXV?l_DN||yWups^6rK$d2!<=8o3Suh5mC!yRj>sHEk{($;Dnq2-|U7E)`xa z{M;|gVaZj4tVJsw)FY$LlfV1)w~ap6j}*$R(A1Zx!&&m|yxOIodZBRW*^sbZ-8oPF z1v1;uN$*^LJmu(Ol`e%7`KPaqcoav91kPzU{iixqiIDYB##T^!u@HxW+{N`GUdG+A zDT-b(Y+fL1%sC%Qe$pVKVQth5O88_)&emWzF-shn;;#uOpgs zg_ruuKj4{9#VLhkCVF4qiqE)_6kA~t%e{K1DbG#rYo@vi96X_Q$mE3G_MAb&F}bZ@ zJ5knwg31d*R%g5G2$=DDN2(RG>49?D_oqfE~T z{TLyi*XWR-b*DQnMLnOF&m%43Rcn5B-ZgiLTgp3QwrqU7_}KAVH$1D)L=8mknR`}r z!f}Uf#g8g0+C1KslfK2BH1lDT{kJ{V)LrPk5@q2LY<}ii|I1UcSN5l=MtxFOKgcJ! zPMAYYB2jm3G5SJG=*BiAUcvC1Y=~Ofs1m+TITsF=DQ}$$8_J(KIu?z`Gkf~^`toDa zwIX33_S&vb7r0JYHNhRW#Z{VLY0h1@p1Duri-YFGc1zi)y&2XCxD=7LL^cE*Nj&+Bh@NA>51?DbpH9N%fs-L-ad;PXI({AgCo$jxle0`p^C{ew}qm+vNhUYYjj z^Vn#S@#N5(;o6Rc%hguzJ>I&GXYVD^+S+?HIj5}J!ppArf6k3LH#t!_Gurkvi^m7e98xUAohPH`zyPHXELm+Cv+R;FCr z)>W{!br@68aeq(R$I}vJ`IU6aV~o;QK!EtNdUQRxUcFwg zeq+6Ahy+H$sG*~r=ZEM<#3qC(!VF=Kus~QMHY2PMTM%0j)(9JfEn*wO4q=aQKx{`i zBAgIZgfqeg;fioWxFb9eo(M05H-dJ6^dRBAQIBlU!ulx68pMnzr80H*WDye7mg}B2@qfkP zcY}LdgZ%O-O_H=oQdvSv`TzVeDj@onFxFDD)bxoBzeAUmz?tVOUOfJvv&<>Bey`ZG zD2P9QZW9-6|9|fhQzKQz^~q|#q(QW!fbSa3NQ)=8799OM{*SCbD)z<)8RW|}iPDbd z{BJ!{YR1bRPFf918B98MO3^5mlVyEsznPJbb~xvu^@sEx%OHh(@uvT8kM484kv@o+ zh9~cw+e_bxz?X(@)g;w1%FA^xmDG~Hx5!>=uxq$dp=4()RU13uA2R$yUMbgG=zgHx#I8c3 zO;^P3=AJzzQV|7i-!dzrM~$qMOTGy$of!Gpy)p!y5Zffg^k%A zw3}s#Ut0EdjqB#sbR+f2*eh_F6Kr7&AM)ilR>0N-5IW@>1QquPV12w09YEAvrx8+k>ALOrV<`4G< z;8R^31lRVad`h|m2rl4FK|QaeL-Y2B-|FBYJmakn5h60)!taf!PHqmGv>?bD4TuB^ z9z`KgC`5>i$Dk+}3WW@&(E>cE-u`|NTn#8%AIqm?;O689S;O`>gZh8C+R}!NPqpSo`mU z!OS8I2G2Y#Q&anU9scRG*x9BfFi*?W*UvibMp%VddQ{M*q^rji!qSz>rb!HrnK%5^ zuk6F3Nz9bX0!=ccV`q+KW)3HSKW2`lCzIcs2{)U*$jsCJ&ea+Ens#w4JyEgC!tBV{ z-#PMsR1{q-cIM3fOxNolD+)u8ZHA@kMu+H&V~-gOhDj^9zp^xT+S0#F0H6zH6AjDk z1GwKgFN@y(bXoxOuuse4*G!G{FJfj|lXUf%LiFXbYZA+>Ni5rj^ULfrwVS~)Lsu&M z!qLoT`YS)fP3O&Dnu$1@zL-6b&gfSjNRJuZRPG0|;OODQE(>$wq7VNSQ&!tgD~cf& zJ9Cy~%!CQfYGD@U%rQeZI>eBLZ4L~D*%*IiY235`zcdDE7GvO8eBgJ^%OcvJPD`F` zT4ukdGy0Xe7(PHxmh|j{o6_WeaBCJ#{?%51o8Af-(y^Hd%bf7>e^n0P|F9gO%%(49 z52Q2tt-kQn`!aol zGH;alzp^xbT7X~13}&`U6UX8Nf0YmZc;4BHLL9Sa(>eXhVDzZLPgfgs1(`y4dbVKG zC5}axf3+Flr#AzJc z#_8*vMHUv%{Tg)k7E9W(MnU`maKZeDHxV6$e1 zYq0Xn=ne1}pc`WcF8~zI9EPD=ka;wkAU}oOuuzGBO5gBUs3bsTtdeA zqt#)d0@)Z%77G=~Hs!|5e1U9J+X^!k$Tn3KGE;$UjEbF^FKBLl#s$+@9FPr1-+fue z0@)a4CJPnF#wc)Es6e)<2xI08WMk}qEMtLeQ`<4~SRfmtR$>_oWSa_E=CMFFM$yJR z78D9o`#TF2$i}FcSg1fYM)0#xfoxMLkZCrs3qe!)fSC$pW8_H|4nVf4q`|ad!m6Fh z^$cK`o(PyT13fFwvK_#g#gs+^Nz?#h#5~BMbnG2|V+;-p45+(cz^#QEx;On99?M#CY6Bup%PYUB5)d>Q}0#qR9 z)cL^A#ZKk;X$27BIG>V^aX$zHVxI(V82#!B5e7pBt;X~nqH=L-Lu=~L=%k#^c9Dq3U*zv798*YWk1b7Y_}L#e8v)C7Y?mt;rmt2za? ztI_lULD z!ZZaMXY?HRg^p*OA}3zjY7hIJ2#myHI22oOW6!+R#TI(IwnP>n zDo=GN{V1^)w+qP)K2{{F+kNOw7LUnz+NIo>+@Vf)0VPCX$k!SRouWoVy9aOM#t2BuPY%mnc=B%79lIOssMJI_ zQpA5}a!Kg~SD8q&`j}PepyJq3SN(M#D^bCm9%d!z1TnjV!AdXWl^^W7B9{2^lksY0 zUS19Jpb%cWxr1^RLE^p~LRw3Y_2IM0jmRC6xiP9-xKxt|95rW-aMin1c7+NLtq8qm zcfwJ@ofbcyFSXuUuoW|kSLHTy-C4{V6gV;>PKoeu$b<}9^tO0q4<%@t6pvDdg?^y8 zcg0hO_i=s~o7~(R06jIg{t~f3y(PX^u&JbAf+xCN<^3cs(n-Nz4JzWu+ZXatrtHbw z?`t~F>!c)E8K}kbX9po}`R-njG_>~S#T@Il8j6;4+^b~EYlGg#^Yph%e;XW{*L!uF z?43BFK=Qd>zc!lU+%=ZgTK%H;iw3xDX&wfuHB0d)3#BuyZ=@edxBT83{j5mj_J_SW z+yYTQu|@-TjS$h z4K*<_k6o)8{0D{{&feOJII=DTBMy-O~kgU=SPbM56G9f$U7^G`jZoNsi=~_4uFB4bi_r$bFTVQ|KRxhi? zs|$1!dlD4mN8(>ose;W8D8)jB;zz9;^^crS%a6HvbbWRPb!vj4cB|de)3CX ztB&DgS#7GHqp#Zza2u3yObgd`kdg-dpMr*`znizWqPD)Vq9)DL35@V`aPfoid`cRA zj-ZZ)NJJcpNWzfGd`d7Qn52ls5m7iS8H0m!FxATvlz=1@kwU=16fK7xdQ>+TSAU2| zL8I_k3XuYK1(56A95uXMJgE?xu?$W6X|N)Oh(TfK%pmYP2}L22Nd&O#V6Z4M1x>_3 zWC|ID$B_u2Duty1xtofG=((MFdtvKA;IWJ^rsap|3IZpAnht(c`ib>_RD)T-c7OWe zIXLBnN5WeX*zmxUIZanz+sI)DI3ITc+HmvVMJJo>@&l^)xpSFPUEKWqeRn}}8sIi8 z74#q`zD`tMH@GGF`57RZ1?<=X&R=1rf=!1{X)|2OTZ4mf6cJ6qk!|71*cwN~qp$=L z8Dk4?{mgHP6!3__qlkDiy|f`=%l zij{z-ph!dl&@(e59DxKZ2aIkDQLvCTo(%371&;8PKZp6?60;}ED6A= z)4Qr|Or!fDwD95XelfrM1y*Bq0qKyYk9E&QwW{x&_$6M+rNS+TY0c7(IhE>MQZ6r@ zd_}9v?`(RlIpFNvbn&ppYKI+XHkXL(mb|iEpHI^_&x|}=<+h08m#4TWMMnUqU=Z5g zcrBcOkX-)gUCXlYZ4v6o>Ul>has%FFY~9y#=$Yn=>wI$rGv4^6*yYY&!FSgYc@EVX z=$}}0EXoLZdH=$*VHI2wjXwDEOJh#nvKBvK;%%|u zEy-&H{AF{$omhDODBnf?O-AN(o;^7gr-W2gdZ;{F{%=T`Qi4Ao63Wd&#bCxxYk z6lDsRws7$kiN^Mx(L0ucT!ZOL6%;MuV&|1yDzxR{n zrr(?1g`VoF_A#%iPbwNHas8j|B>ux z|C#gb(+o}lsr4?NyOwXzQd~W+a@U@xsYmQ?9AE7(T)N@5pk)o$DT%|IkoBRjSI#}8(2E{8 z)hl8CL7*{Xz|vje&SY4~r`>8=PwkrA=8qVU`TqRzO9dQ1$%gt$d z$=&8f$syKz??Y`m0zr!_&BPz(OWb@`j}s}xy`Mrc+-)yk+aHrzaLLK zaR3=)*+0_PosG`u=`?G)YA>B7RNNi->EP)GpGJO(qTH6Aog2-U6h&Md;y>}IK)7gM z*f2q?1l!kQV&CZ=d1Up=3cDlsMq_mX+Oki+=+@jTX0Lgl;aXKMZ?D)-avhXJlo*jeIq|Qtq8TQJe7W)_sc%dpaZ-h7n=iL-;8c8*b;i5b zX=BC1mvi?Dwn>CKMaa$H+V$xCPXEA-ewPwTZUb z(dxaCPMnCe8y_D3YRob7%39$Y*Sk)CTj%e`HZIq6!qukCS}}g9zRTA$ z_$3~V4}5o?Z&aAv|8@ztwefzjp2AO_4-aV2uzbPBHY$teZp+<*kFid^_Q2`=TNO>0 zi1iD~7VG%FJ-pQ5j?9zI7CDh#UlDD#4=P`@YP_s1$T*alVZ6a2VomMCx0*b!OwDXt zYQN^{mhCYN&bb>bw-uwzJu>v*D}Shgyr$sG;-kk?7rJVQE#NAQ{>XUrvAH=C!yV z&HMI=?vs@`zfL!L<;2kZZ$3@-TH89g1XAXPE^yH2R7~kt-RBWt;kuc6_!FNvt$1(W zXYAEuh}29esdkqRoXlDEfa(H8#WgKXy&tMZ>jPxC)MA#Lmu*@VQBWv-!F{5z|m%I&%Jj@W&5U>3EoDbQ(qU9HmN6H%zBCRQgsNg*y?53;T?s4QFk7- z>gAKR@T$2ND$%vyde6Lg6FF!jUG6J_dRC`ju<~vq?>@={M@8+za982@U~-CHkcIsx z+2jRjk4&15XrJjD%M(pA*Rhx?u7-B+6g)D408Zz5kN5bYFGdaIMfk6!Y}_Woo$t>)bcB_9Vr39(!1b zHRoJMS;^sv6PEbGu|!UA`@G{N>1FGD9yqn^-sv^+MPs4}oM7}cXCeF;_D^%i&Y z^>ut&AH424VU?)16md_4W|j4mOWHTpS4Yl64`U8xFMhWEz)>XU2H(145!!N}sv=z- zbn8kGqntT%J zAXitS;n{DJ%_MC75A}#UDMT8 zF-v8U$%N*8vy2ZU^_9|Dn9ZaUTb~_O*l)}yAuq{=`7z|JzjRrmKK6!K+qZMbnqutJ z^9Sd?>0j42*Wre4I!~OHx4m!l=QZ)sTdQ$bt5Q6M#Cy&x+G6<>(k z=nqFbm6MkAUwsqie-kMZ)mWHm@9f>y6(ayB_+hR3} zF3M|^pImrZyO4X0h_6oLo0>IsH}qHFZ_BLF|H!}Hs_+SQO9saGaqj)}>(5Nm8dMT* z-rni7P4sb(<{NqMsv1|e z$4VAUT-H1Bd8wFJ8OPl8zUt$K(xtvT9}a>LS;@C`{pjZrJ@pUWeAi7^TO0Klt6vE} z62BzVr2k>O(W=EIu3alv{@UW}fZVg^BWODk{_<6~0IpAMDRI}Tt{7G16GIKYFF$-v z(0<&buTD}e)^9A|Y))W749CdEl~l)$17GHdZ&};zOKc;%X`hTvIB6AQ@2udIqF@1H4xRK*)|#!^09qmL;Rvlm%2;k z$$5d|U18!J1A;Hp6f$p!iuaod7DO7S``oK}o?@?b%|q>Q!37IDtqm6nFBE1}ov)UO zPc1Ui!`>ZV;6V)gXe(L9JIa$1XPF?qULZ_8>&Tt(3o)D-#4DE77M777k^?>vx8cuXZ;y;e{ou-3;uDrvW5kLKF+ZBMt&Q;vNhD*gSr;++ew z+gnNpyoL^pX?mDt&Htg7gA+2CGyi6*(FQH|&=cX+=Lk+b&qB(Ru4k;Zu{gh6N~*ul z+9z+!J+0(rU4u_{u>0|`U90S3hjx+o2fLM=>nGN|aR!?w`FXWX zP1&0e;?MJ`e-T-@TbS;u8C^C5RnVM?Du9>i{ zYFO}uSrqVZxAuPtB>-^%3VU3On?4)`126{+I6TAyDy({NYD7lC z5CI;bG9(fXAPjhLZ6AY&K^Z&%WzcxQ37mc4P&fcIz;AIV3=zBr>e8rkXz@jKHnDi3` zA)|;eYy`#=08#`S6e5CgcnT2(uoJ)ltO7mt4OkBvMFNNhVE-=-hh=yfnfWaiARTnn z1PkyJhFoYS8L%WQ3JaX<6%~$KN2*LQ@ji>*6*|irUD?XjF}&A#aa@(xo2?6j`dU|2YrH>rtG-0pHlHix zK=v&|y^ILH&4*}&?~yXE-YNQp-nvur=vi$4CzUL}9{y(KuV3cuT(fI%Y5pyj!r>cv z|Kx@HHKPv*C{ryS6Bb~;+~27*K2KSr;cj&d$)QfCBI%=}PqEB)jmA{{l5e}`7UsCv zQl;&hL*bkHgA4EjA`%GTkk%Rcmj)lT!~Y=fpEMkZ zi*lkI~vc(LVh=&iP6;GiW6EN(?IvY>RB) zlJID@7$lM4%Ox|u1&WzT1FC0}4O9qh{Ky#gSS8OAGZZX)E&(L~Tf9*S=$UrIph?7; z@?tP(HXUFvI5uAcr}%7sfC1&pOnEWDPiM-8A&}t + + + + A rule giving user with role REGNA or DAGL the right to instantiate a instance of a given app of tdd/contributer-restriction + + + + + REGNA + + + + + + DAGL + + + + + + + + tdd + + + + contributer-restriction + + + + + + + + instantiate + + + + + + + + Eksempel på samleregel som spesifiserer at både REGNA og DAGL m/sikkerhetsnivå; 2, for ressursen; SKD/TaxReport får tilgang til operasjonene; Read, Write og Instantiate både for Event; Tasks; FormFilling og Signing + + + + + REGNA + + + + + + DAGL + + + + + + + + tdd + + + + contributer-restriction + + + + Task_1 + + + + + + + + read + + + + + + write + + + + + + + + Eksempel på tilleggsregel som spesifiserer at bare DAGL m/sikkerhetsnivå; 2, for ressursen; SKD/TaxReport får tilgang til operasjonen; Sign både for Event; SluttEvent_1b + + + + + DAGL + + + + + + + + tdd + + + + contributer-restriction + + + + signing + + + + + + + + sign + + + + + + + + Example rule that gives org nav and skd read right to the app inn all states + + + + + tdd + + + + + + + + tdd + + + + + + + + tdd + + + + contributer-restriction + + + + + + + + read + + + + + + + + Example rule that gives org write right for app inn all states + + + + + tdd + + + + + + 160694123 + + + + + + + + tdd + + + + contributer-restriction + + + + + + + + write + + + + + + + + Rule that defines that org can complete an instance of tdd/contributer-restriction which state is at the end event. + + + + + tdd + + + + + + + + tdd + + + + contributer-restriction + + + + EndEvent_1 + + + + + + + + complete + + + + + + + + + + 2 + + + + diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/process/process.bpmn b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/process/process.bpmn new file mode 100644 index 000000000..f28219543 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/process/process.bpmn @@ -0,0 +1,45 @@ + + + + + SequenceFlow_1n56yn5 + + + SequenceFlow_1n56yn5 + SequenceFlow_1oot28q + + + SequenceFlow_1oot28q + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/texts/resource.nb.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/texts/resource.nb.json new file mode 100644 index 000000000..9c9ca4534 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/texts/resource.nb.json @@ -0,0 +1,169 @@ +{ + "language": "nb-NO", + "resources": [ + { + "id": "Radioknapp.Label", + "value": "Radioknapp komponent for automatisert testing" + }, + { + "id": "34882.MelderMalformdatadef34882.Label", + "value": "Målform" + }, + { + "id": "34883.SignererTredjeMalformdatadef34883.Label", + "value": "Målform" + }, + { + "id": "34884.SignererTredjeFodselsnummerdatadef34884.Label", + "value": "Fødselsnummer" + }, + { + "id": "34885.SignererTredjeEtternavndatadef34885.Label", + "value": "Etternavn" + }, + { + "id": "34886.SignererTredjeFornavndatadef34886.Label", + "value": "Fornavn" + }, + { + "id": "34887.SignererTredjeMellomnavndatadef34887.Label", + "value": "Mellomnavn" + }, + { + "id": "34889.SignererTredjeEpostdatadef34889.Label", + "value": "epost" + }, + { + "id": "34890.SignererTredjeMobiltelefonnummerdatadef34890.Label", + "value": "Mobiltelefonnummer" + }, + { + "id": "34891.SignererTredjeReferanseAltinndatadef34891.Label", + "value": "Referansenummer Altinn" + }, + { + "id": "AppTittel", + "value": "Endring av navn" + }, + { + "id": "AutomatedTest_Radioknapp.Label", + "value": "Radioknapp komponent for automatisert testing" + }, + { + "id": "BegrunnelseAnnet", + "value": "Forklar hvorfor du ønsker å ta navnet, og eventuelt hvilken tilknytning du har til navnet:" + }, + { + "id": "BegrunnelseGard1", + "value": "Gårdsbruk du vil ta navnet fra" + }, + { + "id": "BegrunnelseGard2", + "value": "Kommune gårdsbruket ligger i" + }, + { + "id": "BegrunnelseGard3", + "value": "Gårdsnummer" + }, + { + "id": "BegrunnelseGard4", + "value": "Bruksnummer" + }, + { + "id": "BegrunnelseGard5", + "value": "Forklar din tilknytning til gårdsbruket" + }, + { + "id": "BegrunnelseNyttNavn", + "value": "Du ønsker å ta et navn som du tror er nytt i Norge. Forklar hvorfor du ønsker dette navnet:" + }, + { + "id": "BegrunnelseSamboer1", + "value": "Etternavn på samboer" + }, + { + "id": "BegrunnelseSamboer2", + "value": "Fødselsnummer på samboer" + }, + { + "id": "BegrunnelseSlektskap", + "value": "Forklar hvem du tar navnet fra, og hvilken side slektskapet ligger" + }, + { + "id": "BegrunnelseSteforeldre", + "value": "Etternavn på ste- eller fosterforeldre du ønsker å ta navnet til" + }, + { + "id": "BegrunnelseValgNavn", + "value": "Vennligst oppgi begrunnelse for endring av navn" + }, + { + "id": "BekreftNavnTittel", + "value": "Bekreftelse av navn" + }, + { + "id": "DineEndringer", + "value": "Dine endringer" + }, + { + "id": "EndreNavnFra", + "value": "Du har valgt å endre:" + }, + { + "id": "EndreNavnTil", + "value": "Til:" + }, + { + "id": "Epost", + "value": "E-post" + }, + { + "id": "Fodselsnummer", + "value": "Fødselsnummer" + }, + { + "id": "HintEtternavn", + "value": "Bindestrek må benyttes dersom du ønsker to etternavn." + }, + { + "id": "HintMellomnavn", + "value": "Mellomnavn må være et navn som kan benyttes som etternavn. " + }, + { + "id": "Kontaktinformasjon", + "value": "Kontaktinformasjon" + }, + { + "id": "NavarendeNavn", + "value": "Nåværende navn" + }, + { + "id": "PersonNyttEtternavn", + "value": "Nytt etternavn" + }, + { + "id": "PersonNyttFornavn", + "value": "Nytt fornavn" + }, + { + "id": "PersonNyttMellomnavn", + "value": "Nytt mellomnavn" + }, + { + "id": "ServiceName", + "value": "Contribuer restriction app" + }, + { + "id": "Skjema.Label", + "value": "Radioknapp komponent for automatisert testing" + }, + { + "id": "Telefonnummer", + "value": "Telefonnummer" + }, + { + "id": "kontaktinfoBeskrivelse", + "value": "Kontaktinformasjon dersom saksbehandler ønsker å ta kontakt vedrørende denne saken" + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/appsettings.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/appsettings.json similarity index 100% rename from test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/appsettings.json rename to test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/appsettings.json diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/config/applicationmetadata.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/config/applicationmetadata.json similarity index 100% rename from test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/config/applicationmetadata.json rename to test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/config/applicationmetadata.json diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/config/authorization/policy.xml b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/config/authorization/policy.xml similarity index 100% rename from test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/config/authorization/policy.xml rename to test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/config/authorization/policy.xml diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/config/process/process.bpmn b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/config/process/process.bpmn similarity index 100% rename from test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/config/process/process.bpmn rename to test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/config/process/process.bpmn diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/config/texts/resource.nb.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/config/texts/resource.nb.json similarity index 100% rename from test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/config/texts/resource.nb.json rename to test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/config/texts/resource.nb.json diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/logic/App.cs b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/App.cs similarity index 59% rename from test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/logic/App.cs rename to test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/App.cs index d543087f4..3a639c958 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/logic/App.cs +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/App.cs @@ -1,4 +1,3 @@ -using System; using Altinn.App.Core.Internal.AppModel; using Microsoft.Extensions.Logging; @@ -23,18 +22,27 @@ public App(ILogger logger) /// public object Create(string classRef) { - _logger.LogInformation($"CreateNewAppModel {classRef}"); + _logger.LogInformation("CreateNewAppModel {classRef}", classRef); Type? appType = Type.GetType(classRef); - return Activator.CreateInstance(appType); + + if (appType == null) + { + throw new ArgumentException($"Could not find type {classRef}"); + } + + object? appInstance = Activator.CreateInstance(appType); + return appInstance ?? throw new ArgumentException($"Could not create instance of {classRef}"); } /// public Type GetModelType(string classRef) { - _logger.LogInformation($"GetAppModelType {classRef}"); + _logger.LogInformation("GetAppModelType {classRef}", classRef); +#pragma warning disable CS8603 // Possible null reference return. return Type.GetType(classRef); +#pragma warning restore CS8603 // Possible null reference return. } } } diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/logic/EFormidlingMetadata.cs b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/EFormidlingMetadata.cs similarity index 100% rename from test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/logic/EFormidlingMetadata.cs rename to test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/logic/EFormidlingMetadata.cs diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/models/Skjema.cs b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/models/Skjema.cs similarity index 100% rename from test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/models/Skjema.cs rename to test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/models/Skjema.cs diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/models/Skjema.xsd b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/models/Skjema.xsd similarity index 100% rename from test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/models/Skjema.xsd rename to test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/models/Skjema.xsd diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/ui/RuleHandler.js b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/ui/RuleHandler.js similarity index 100% rename from test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/ui/RuleHandler.js rename to test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/ui/RuleHandler.js diff --git a/test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/ui/layouts/FormLayout.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/ui/layouts/FormLayout.json similarity index 100% rename from test/Altinn.App.Api.Tests/Data/apps/ttd/eformidling-app/ui/layouts/FormLayout.json rename to test/Altinn.App.Api.Tests/Data/apps/tdd/eformidling-app/ui/layouts/FormLayout.json diff --git a/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs b/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs index 0d85bf6eb..723fd6a3f 100644 --- a/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs +++ b/test/Altinn.App.Api.Tests/EFormidling/EformidlingStatusCheckEventHandlerTests.cs @@ -6,76 +6,71 @@ using Altinn.App.Core.Infrastructure.Clients.Maskinporten; using Altinn.App.Core.Models; using Altinn.Common.EFormidlingClient; -using Altinn.Common.PEP.Configuration; using FluentAssertions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using System; -using System.Net.Http; -using System.Threading.Tasks; using Xunit; -namespace Altinn.App.Api.Tests.EFormidling +namespace Altinn.App.Api.Tests.EFormidling; + +public class EformidlingStatusCheckEventHandlerTests { - public class EformidlingStatusCheckEventHandlerTests + [Fact] + public async Task ProcessEvent_NoStatuses_ShouldReturnFalse() { - [Fact] - public async Task ProcessEvent_NoStatuses_ShouldReturnFalse() - { - EformidlingStatusCheckEventHandler eventHandler = GetMockedEventHandler(); - CloudEvent cloudEvent = GetValidCloudEvent(); + EformidlingStatusCheckEventHandler eventHandler = GetMockedEventHandler(); + CloudEvent cloudEvent = GetValidCloudEvent(); - bool processStatus = await eventHandler.ProcessEvent(cloudEvent); + bool processStatus = await eventHandler.ProcessEvent(cloudEvent); - processStatus.Should().BeFalse(); - } + processStatus.Should().BeFalse(); + } - private static CloudEvent GetValidCloudEvent() + private static CloudEvent GetValidCloudEvent() + { + return new() { - return new() - { - Id = Guid.NewGuid().ToString(), - Source = new Uri("https://dihe.apps.altinn3local.no/dihe/redusert-foreldrebetaling-bhg/instances/510002/553a3ddc-4ca4-40af-9c2a-1e33e659c7e7"), - SpecVersion = "1.0", - Type = "app.eformidling.reminder.checkinstancestatus", - Subject = "/party/510002", - Time = DateTime.Parse("2022-10-13T09:33:46.6330634Z"), - AlternativeSubject = "/person/17858296439" - }; - } + Id = Guid.NewGuid().ToString(), + Source = new Uri("https://dihe.apps.altinn3local.no/dihe/redusert-foreldrebetaling-bhg/instances/510002/553a3ddc-4ca4-40af-9c2a-1e33e659c7e7"), + SpecVersion = "1.0", + Type = "app.eformidling.reminder.checkinstancestatus", + Subject = "/party/510002", + Time = DateTime.Parse("2022-10-13T09:33:46.6330634Z"), + AlternativeSubject = "/person/17858296439" + }; + } - private static EformidlingStatusCheckEventHandler GetMockedEventHandler() - { - var eFormidlingClientMock = new Mock(); - var httpClientFactoryMock = new Mock(); - var eFormidlingLoggerMock = new Mock>(); - - var httpClientMock = new Mock(); - var maskinportenServiceLoggerMock = new Mock>(); - var tokenCacheProviderMock = new Mock(); - var maskinportenServiceMock = new Mock(httpClientMock.Object, maskinportenServiceLoggerMock.Object, tokenCacheProviderMock.Object); + private static EformidlingStatusCheckEventHandler GetMockedEventHandler() + { + var eFormidlingClientMock = new Mock(); + var httpClientFactoryMock = new Mock(); + var eFormidlingLoggerMock = new Mock>(); - var maskinportenSettingsMock = new Mock>(); - var x509CertificateProviderMock = new Mock(); - IOptions platformSettingsMock = Options.Create(new Altinn.App.Core.Configuration.PlatformSettings() - { - ApiEventsEndpoint = "http://localhost:5101/events/api/v1/", - SubscriptionKey = "key" - }); - var generalSettingsMock = new Mock>(); + var httpClientMock = new Mock(); + var maskinportenServiceLoggerMock = new Mock>(); + var tokenCacheProviderMock = new Mock(); + var maskinportenServiceMock = new Mock(httpClientMock.Object, maskinportenServiceLoggerMock.Object, tokenCacheProviderMock.Object); + + var maskinportenSettingsMock = new Mock>(); + var x509CertificateProviderMock = new Mock(); + IOptions platformSettingsMock = Options.Create(new Altinn.App.Core.Configuration.PlatformSettings() + { + ApiEventsEndpoint = "http://localhost:5101/events/api/v1/", + SubscriptionKey = "key" + }); + var generalSettingsMock = new Mock>(); - EformidlingStatusCheckEventHandler eventHandler = new( - eFormidlingClientMock.Object, - httpClientFactoryMock.Object, - eFormidlingLoggerMock.Object, - maskinportenServiceMock.Object, - maskinportenSettingsMock.Object, - x509CertificateProviderMock.Object, - platformSettingsMock, - generalSettingsMock.Object - ); - return eventHandler; - } + EformidlingStatusCheckEventHandler eventHandler = new( + eFormidlingClientMock.Object, + httpClientFactoryMock.Object, + eFormidlingLoggerMock.Object, + maskinportenServiceMock.Object, + maskinportenSettingsMock.Object, + x509CertificateProviderMock.Object, + platformSettingsMock, + generalSettingsMock.Object + ); + return eventHandler; } } diff --git a/test/Altinn.App.Api.Tests/Helpers/RequestHandling/DataRestrictionValidationTests.cs b/test/Altinn.App.Api.Tests/Helpers/RequestHandling/DataRestrictionValidationTests.cs index 7383dc74d..730bba515 100644 --- a/test/Altinn.App.Api.Tests/Helpers/RequestHandling/DataRestrictionValidationTests.cs +++ b/test/Altinn.App.Api.Tests/Helpers/RequestHandling/DataRestrictionValidationTests.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Configuration; using Altinn.App.Api.Helpers.RequestHandling; +using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.AspNetCore.Http; @@ -14,13 +16,13 @@ public class DataRestrictionValidationTests public void CompliesWithDataRestrictions_returns_false_with_badrequest_if_contentdisposition_not_set() { var httpContext = new DefaultHttpContext(); - bool valid = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, new DataType(), out ActionResult? errorResponse); + (bool valid, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, new DataType()); valid.Should().BeFalse(); - errorResponse.Should().NotBeNull(); - errorResponse.Should().BeOfType(typeof(BadRequestObjectResult)); - ((BadRequestObjectResult)errorResponse!).Value.Should().BeEquivalentTo("Invalid data provided. Error: The request must include a Content-Disposition header"); + errors.Should().NotBeNull(); + errors.FirstOrDefault().Should().BeOfType(typeof(ValidationIssue)); + errors.FirstOrDefault()?.Description.Should().BeEquivalentTo("Invalid data provided. Error: The request must include a Content-Disposition header"); } - + [Fact] public void CompliesWithDataRestrictions_returns_false_with_413_status_if_contentlength_greater_than_maxSize() { @@ -31,14 +33,13 @@ public void CompliesWithDataRestrictions_returns_false_with_413_status_if_conten { MaxSize = 1 }; - bool valid = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType, out ActionResult? errorResponse); + (bool valid, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType); valid.Should().BeFalse(); - errorResponse.Should().NotBeNull(); - errorResponse.Should().BeOfType(typeof(ObjectResult)); - ((ObjectResult)errorResponse!).StatusCode.Should().Be(413); - ((ObjectResult)errorResponse).Value.Should().BeEquivalentTo("Invalid data provided. Error: Binary attachment exceeds limit of 1048576"); + errors.Should().NotBeNull(); + errors.Should().BeOfType(typeof(List)); + errors.FirstOrDefault()!.Description.Should().BeEquivalentTo("Invalid data provided. Error: Binary attachment exceeds limit of 1048576"); } - + [Fact] public void CompliesWithDataRestrictions_returns_false_with_badrequest_status_if_filename_not_supplied() { @@ -49,13 +50,13 @@ public void CompliesWithDataRestrictions_returns_false_with_badrequest_status_if { MaxSize = 1 }; - bool valid = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType, out ActionResult? errorResponse); + (bool valid, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType); valid.Should().BeFalse(); - errorResponse.Should().NotBeNull(); - errorResponse.Should().BeOfType(typeof(BadRequestObjectResult)); - ((BadRequestObjectResult)errorResponse!).Value.Should().BeEquivalentTo("Invalid data provided. Error: The Content-Disposition header must contain a filename"); + errors.Should().NotBeNull(); + errors.Should().BeOfType(typeof(List)); + errors.FirstOrDefault()!.Description.Should().BeEquivalentTo("Invalid data provided. Error: The Content-Disposition header must contain a filename"); } - + [Fact] public void CompliesWithDataRestrictions_returns_false_with_badrequest_status_if_filename_without_extension() { @@ -66,13 +67,13 @@ public void CompliesWithDataRestrictions_returns_false_with_badrequest_status_if { MaxSize = 1 }; - bool valid = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType, out ActionResult? errorResponse); + (bool valid, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType); valid.Should().BeFalse(); - errorResponse.Should().NotBeNull(); - errorResponse.Should().BeOfType(typeof(BadRequestObjectResult)); - ((BadRequestObjectResult)errorResponse!).Value.Should().BeEquivalentTo("Invalid data provided. Error: Invalid format for filename: test. Filename is expected to end with '.{filetype}'."); + errors.Should().NotBeNull(); + errors.Should().BeOfType(typeof(List)); + errors.FirstOrDefault()!.Description.Should().BeEquivalentTo("Invalid data provided. Error: Invalid format for filename: test. Filename is expected to end with '.{filetype}'."); } - + [Fact] public void CompliesWithDataRestrictions_returns_true_if_contentdisposition_filesize_and_no_allowed_datatypes_set_on_datatype() { @@ -83,11 +84,11 @@ public void CompliesWithDataRestrictions_returns_true_if_contentdisposition_file { MaxSize = 1 }; - bool valid = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType, out ActionResult? errorResponse); + (bool valid, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType); valid.Should().BeTrue(); - errorResponse.Should().BeNull(); + errors.Should().BeEmpty(); } - + [Fact] public void CompliesWithDataRestrictions_returns_true_if_contentdisposition_filesize_and_emptylist_allowed_datatypes_set_on_datatype() { @@ -99,11 +100,11 @@ public void CompliesWithDataRestrictions_returns_true_if_contentdisposition_file MaxSize = 1, AllowedContentTypes = new List() }; - bool valid = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType, out ActionResult? errorResponse); + (bool valid, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType); valid.Should().BeTrue(); - errorResponse.Should().BeNull(); + errors.Should().BeEmpty(); } - + [Fact] public void CompliesWithDataRestrictions_returns_false_if_contenttype_not_set_and_allowedcontenttypes_defined() { @@ -113,15 +114,15 @@ public void CompliesWithDataRestrictions_returns_false_if_contenttype_not_set_an var dataType = new DataType() { MaxSize = 1, - AllowedContentTypes = new List(){"application/pdf"} + AllowedContentTypes = new List() { "application/pdf" } }; - bool valid = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType, out ActionResult? errorResponse); + (bool valid, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType); valid.Should().BeFalse(); - errorResponse.Should().NotBeNull(); - errorResponse.Should().BeOfType(typeof(BadRequestObjectResult)); - ((BadRequestObjectResult)errorResponse!).Value.Should().BeEquivalentTo("Invalid data provided. Error: Content-Type header must be included in request."); + errors.Should().NotBeNull(); + errors.Should().BeOfType(typeof(List)); + errors.FirstOrDefault()!.Description.Should().BeEquivalentTo("Invalid data provided. Error: Content-Type header must be included in request."); } - + [Fact] public void CompliesWithDataRestrictions_returns_false_if_contenttype_not_defined_in_allowedcontenttypes() { @@ -132,15 +133,15 @@ public void CompliesWithDataRestrictions_returns_false_if_contenttype_not_define var dataType = new DataType() { MaxSize = 1, - AllowedContentTypes = new List(){"application/pdf"} + AllowedContentTypes = new List() { "application/pdf" } }; - bool valid = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType, out ActionResult? errorResponse); + (bool valid, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType); valid.Should().BeFalse(); - errorResponse.Should().NotBeNull(); - errorResponse.Should().BeOfType(typeof(BadRequestObjectResult)); - ((BadRequestObjectResult)errorResponse!).Value.Should().BeEquivalentTo("Invalid data provided. Error: Invalid content type: application/json. Please try another file. Permitted content types include: application/pdf"); + errors.Should().NotBeNull(); + errors.Should().BeOfType(typeof(List)); + errors.FirstOrDefault()!.Description.Should().BeEquivalentTo("Invalid data provided. Error: Invalid content type: application/json. Please try another file. Permitted content types include: application/pdf"); } - + [Fact] public void CompliesWithDataRestrictions_returns_false_if_fileextension_not_matching_contenttype() { @@ -151,15 +152,15 @@ public void CompliesWithDataRestrictions_returns_false_if_fileextension_not_matc var dataType = new DataType() { MaxSize = 1, - AllowedContentTypes = new List(){"application/pdf", "application/json"} + AllowedContentTypes = new List() { "application/pdf", "application/json" } }; - bool valid = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType, out ActionResult? errorResponse); + (bool valid, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType); valid.Should().BeFalse(); - errorResponse.Should().NotBeNull(); - errorResponse.Should().BeOfType(typeof(BadRequestObjectResult)); - ((BadRequestObjectResult)errorResponse!).Value.Should().BeEquivalentTo("Invalid data provided. Error: Content type header application/pdf does not match mime type application/json for uploaded file. Please fix header or upload another file."); + errors.Should().NotBeNull(); + errors.Should().BeOfType(typeof(List)); + errors.FirstOrDefault()!.Description.Should().BeEquivalentTo("Invalid data provided. Error: Content type header application/pdf does not match mime type application/json for uploaded file. Please fix header or upload another file."); } - + [Fact] public void CompliesWithDataRestrictions_returns_true_when_all_checks_pass() { @@ -170,13 +171,13 @@ public void CompliesWithDataRestrictions_returns_true_when_all_checks_pass() var dataType = new DataType() { MaxSize = 1, - AllowedContentTypes = new List(){"application/pdf", "application/json"} + AllowedContentTypes = new List() { "application/pdf", "application/json" } }; - bool valid = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType, out ActionResult? errorResponse); + (bool valid, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType); valid.Should().BeTrue(); - errorResponse.Should().BeNull(); + errors.Should().BeEmpty(); } - + [Fact] public void CompliesWithDataRestrictions_returns_true_when_octetstream_in_allow_list() { @@ -187,13 +188,13 @@ public void CompliesWithDataRestrictions_returns_true_when_octetstream_in_allow_ var dataType = new DataType() { MaxSize = 1, - AllowedContentTypes = new List(){"application/pdf", "application/octet-stream"} + AllowedContentTypes = new List() { "application/pdf", "application/octet-stream" } }; - bool valid = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType, out ActionResult? errorResponse); + (bool valid, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType); valid.Should().BeTrue(); - errorResponse.Should().BeNull(); + errors.Should().BeEmpty(); } - + [Fact] public void CompliesWithDataRestrictions_returns_true_when_octetstream_in_allow_list_and_content_type_is_octetstream() { @@ -204,10 +205,10 @@ public void CompliesWithDataRestrictions_returns_true_when_octetstream_in_allow_ var dataType = new DataType() { MaxSize = 1, - AllowedContentTypes = new List(){"application/pdf", "application/octet-stream"} + AllowedContentTypes = new List() { "application/pdf", "application/octet-stream" } }; - bool valid = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType, out ActionResult? errorResponse); + (bool valid, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(httpContext.Request, dataType); valid.Should().BeTrue(); - errorResponse.Should().BeNull(); + errors.Should().BeEmpty(); } } diff --git a/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs b/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs new file mode 100644 index 000000000..95ebb5426 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/AppMetadataMock.cs @@ -0,0 +1,143 @@ +using System.Text.Json; +using System.Text; +using Altinn.App.Api.Tests.Mocks; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Options; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Altinn.App.Api.Tests.Data; + +namespace App.IntegrationTests.Mocks.Services +{ + public class AppMetadataMock : IAppMetadata + { + private readonly AppSettings _settings; + private readonly IFrontendFeatures _frontendFeatures; + private ApplicationMetadata? _application; + private readonly IHttpContextAccessor _contextAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// The app repository settings. + /// Application features service + public AppMetadataMock( + IOptions settings, + IFrontendFeatures frontendFeatures, + IHttpContextAccessor httpContextAccessor) + { + _settings = settings.Value; + _frontendFeatures = frontendFeatures; + _contextAccessor = httpContextAccessor; + } + + /// + /// Thrown if deserialization fails + /// Thrown if applicationmetadata.json file not found + public async Task GetApplicationMetadata() + { + // Cache application metadata + if (_application != null) + { + return _application; + } + + if (_contextAccessor.HttpContext == null) + { + throw new Exception("HttpContext is null"); + } + + AppIdentifier appIdentifier = AppIdentifier.CreateFromUrl(_contextAccessor.HttpContext.Request.GetDisplayUrl()); + string filename = TestData.GetApplicationMetadataPath(appIdentifier.Org, appIdentifier.App); + + try + { + if (File.Exists(filename)) + { + JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true + }; + using FileStream fileStream = File.OpenRead(filename); + var application = await JsonSerializer.DeserializeAsync(fileStream, jsonSerializerOptions); + if (application == null) + { + throw new ApplicationConfigException($"Deserialization returned null, Could indicate problems with deserialization of {filename}"); + } + + application.Features = await _frontendFeatures.GetFrontendFeatures(); + _application = application; + + return _application; + } + + throw new ApplicationConfigException($"Unable to locate application metadata file: {filename}"); + } + catch (JsonException ex) + { + throw new ApplicationConfigException($"Something went wrong when parsing application metadata file: {filename}", ex); + } + } + + /// + public async Task GetApplicationXACMLPolicy() + { + string filename = Path.Join(_settings.AppBasePath, _settings.ConfigurationFolder, _settings.AuthorizationFolder, _settings.ApplicationXACMLPolicyFileName); + if (File.Exists(filename)) + { + return await File.ReadAllTextAsync(filename, Encoding.UTF8); + } + + throw new FileNotFoundException($"XACML file {filename} not found"); + } + + /// + public async Task GetApplicationBPMNProcess() + { + string filename = Path.Join(_settings.AppBasePath, _settings.ConfigurationFolder, _settings.ProcessFolder, _settings.ProcessFileName); + if (File.Exists(filename)) + { + return await File.ReadAllTextAsync(filename, Encoding.UTF8); + } + + throw new ApplicationConfigException($"Unable to locate application process file: {filename}"); + } + + public static Task GetApplication(string org, string app) + { + return Task.FromResult(GetTestApplication(org, app)); + } + + private static Application GetTestApplication(string org, string app) + { + string applicationPath = Path.Combine(GetMetadataPath(), org, app, "applicationmetadata.json"); + if (File.Exists(applicationPath)) + { + string content = File.ReadAllText(applicationPath) ?? throw new Exception($"Unable to read application metadata file for {org}/{app}. Tried path: {applicationPath}"); + + if (JsonSerializer.Deserialize(content) is not Application application) + { + throw new Exception($"Unable to deserialize application metadata file for {org}/{app}. Tried path: {applicationPath}"); + } + + return application; + } + + throw new Exception($"Unable to locate application metadata file for {org}/{app}. Tried path: {applicationPath}"); + } + + private static string GetMetadataPath() + { + var uri = new Uri(typeof(InstanceClientMockSi).Assembly.Location); + string unitTestFolder = Path.GetDirectoryName(uri.LocalPath) ?? throw new Exception($"Unable to locate path {uri.LocalPath}"); + + return Path.Combine(unitTestFolder, @"../../../Data/Metadata"); + } + } +} diff --git a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs new file mode 100644 index 000000000..9bbc2ab2b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs @@ -0,0 +1,392 @@ +using System.Text.Json; +using System.Xml.Serialization; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Core.Extensions; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.Http; + + +namespace App.IntegrationTests.Mocks.Services +{ + public class DataClientMock : IDataClient + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAppMetadata _appMetadata; + private static readonly JsonSerializerOptions _serializerOptions = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public DataClientMock(IAppMetadata appMetadata, IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + _appMetadata = appMetadata; + } + + public async Task DeleteBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid) + { + return await DeleteData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, false); + } + + public async Task DeleteData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid, bool delay) + { + await Task.CompletedTask; + string dataElementPath = TestData.GetDataElementPath(org, app, instanceOwnerPartyId, instanceGuid, dataGuid); + + if (delay) + { + string fileContent = await File.ReadAllTextAsync(dataElementPath); + + if (fileContent == null) + { + return false; + + } + + if (JsonSerializer.Deserialize(fileContent, _serializerOptions) is not DataElement dataElement) + { + throw new Exception($"Unable to deserialize data element for org: {org}/{app} party: {instanceOwnerPartyId} instance: {instanceGuid} data: {dataGuid}. Tried path: {dataElementPath}"); + } + + dataElement.DeleteStatus = new() + { + IsHardDeleted = true, + HardDeleted = DateTime.UtcNow + }; + + WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); + + return true; + } + else + { + string dataBlobPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataGuid); + + if (File.Exists(dataElementPath)) + { + File.Delete(dataElementPath); + } + + if (File.Exists(dataBlobPath)) + { + File.Delete(dataBlobPath); + } + + return true; + } + } + + public Task GetBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataId) + { + string dataPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataId); + + Stream ms = new MemoryStream(); + using (FileStream file = new(dataPath, FileMode.Open, FileAccess.Read)) + { + file.CopyTo(ms); + } + + return Task.FromResult(ms); + } + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + public async Task> GetBinaryDataList(string org, string app, int instanceOwnerPartyId, Guid instanceGuid) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + var dataElements = GetDataElements(org, app, instanceOwnerPartyId, instanceGuid); + List list = new(); + foreach (DataElement dataElement in dataElements) + { + AttachmentList al = new() + { + Type = dataElement.DataType, + Attachments = new List() + { + new Attachment() + { + Id = dataElement.Id, + Name = dataElement.Filename, + Size = dataElement.Size + } + } + }; + list.Add(al); + } + + return list; + } + + public Task GetFormData(Guid instanceGuid, Type type, string org, string app, int instanceOwnerPartyId, Guid dataId) + { + string dataPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataId); + + XmlSerializer serializer = new(type); + try + { + using FileStream sourceStream = File.Open(dataPath, FileMode.OpenOrCreate); + + var formData = serializer.Deserialize(sourceStream); + return formData != null ? Task.FromResult(formData) : throw new Exception("Unable to deserialize form data"); + } + catch + { + var formData = Activator.CreateInstance(type); + if (formData != null) Task.FromResult(formData); + throw; + } + } + + public async Task InsertFormData(Instance instance, string dataType, T dataToSerialize, Type type) + { + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + string app = instance.AppId.Split("/")[1]; + string org = instance.Org; + int instanceOwnerId = int.Parse(instance.InstanceOwner.PartyId); + + return await InsertFormData(dataToSerialize, instanceGuid, type, org, app, instanceOwnerId, dataType); + } + + public Task InsertFormData(T dataToSerialize, Guid instanceGuid, Type type, string org, string app, int instanceOwnerPartyId, string dataType) + { + Guid dataGuid = Guid.NewGuid(); + string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerPartyId, instanceGuid); + + DataElement dataElement = new() { Id = dataGuid.ToString(), InstanceGuid = instanceGuid.ToString(), DataType = dataType, ContentType = "application/xml", }; + + try + { + Directory.CreateDirectory(dataPath + @"blob"); + + using (Stream stream = File.Open(dataPath + @"blob/" + dataGuid, FileMode.Create, FileAccess.ReadWrite)) + { + XmlSerializer serializer = new(type); + serializer.Serialize(stream, dataToSerialize); + } + + WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); + } + catch +#pragma warning disable S108 // Nested blocks of code should not be left empty + { + } +#pragma warning restore S108 // Nested blocks of code should not be left empty + + return Task.FromResult(dataElement); + } + + public Task UpdateData(T dataToSerialize, Guid instanceGuid, Type type, string org, string app, int instanceOwnerPartyId, Guid dataGuid) + { + string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerPartyId, instanceGuid); + + DataElement? dataElement = GetDataElements(org, app, instanceOwnerPartyId, instanceGuid).FirstOrDefault(de => de.Id == dataGuid.ToString()); + + if (dataElement == null) + { + throw new Exception($"Unable to find data element for org: {org}/{app} party: {instanceOwnerPartyId} instance: {instanceGuid} data: {dataGuid}"); + } + + Directory.CreateDirectory(dataPath + @"blob"); + + using (Stream stream = File.Open(dataPath + $@"blob{Path.DirectorySeparatorChar}" + dataGuid, FileMode.Create, FileAccess.ReadWrite)) + { + XmlSerializer serializer = new(type); + serializer.Serialize(stream, dataToSerialize); + } + + dataElement.LastChanged = DateTime.Now; + WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); + + return Task.FromResult(dataElement); + } + + public async Task InsertBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, string dataType, HttpRequest request) + { + Guid dataGuid = Guid.NewGuid(); + string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerPartyId, instanceGuid); + DataElement dataElement = new() { Id = dataGuid.ToString(), InstanceGuid = instanceGuid.ToString(), DataType = dataType, ContentType = request.ContentType }; + + if (!Directory.Exists(Path.GetDirectoryName(dataPath))) + { + var directory = Path.GetDirectoryName(dataPath) ?? throw new Exception($"Unable to get directory name from path {dataPath}"); + + Directory.CreateDirectory(directory); + } + + Directory.CreateDirectory(dataPath + @"blob"); + + long filesize; + + using (Stream streamToWriteTo = File.Open(dataPath + @"blob/" + dataGuid, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + await request.Body.CopyToAsync(streamToWriteTo); + streamToWriteTo.Flush(); + filesize = streamToWriteTo.Length; + streamToWriteTo.Close(); + } + + dataElement.Size = filesize; + + WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); + + return dataElement; + } + + public async Task InsertBinaryData(string instanceId, string dataType, string contentType, string filename, Stream stream) + { + Application application = await _appMetadata.GetApplicationMetadata(); + var instanceIdParts = instanceId.Split("/"); + + Guid dataGuid = Guid.NewGuid(); + + string org = application.Org; + string app = application.Id.Split("/")[1]; + int instanceOwnerId = int.Parse(instanceIdParts[0]); + Guid instanceGuid = Guid.Parse(instanceIdParts[1]); + + string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerId, instanceGuid); + + DataElement dataElement = new() { Id = dataGuid.ToString(), InstanceGuid = instanceGuid.ToString(), DataType = dataType, ContentType = contentType, }; + + if (!Directory.Exists(Path.GetDirectoryName(dataPath))) + { + var directory = Path.GetDirectoryName(dataPath); + if (directory != null) Directory.CreateDirectory(directory); + } + + Directory.CreateDirectory(dataPath + @"blob"); + + long filesize; + + using (Stream streamToWriteTo = File.Open(dataPath + @"blob/" + dataGuid, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + stream.Seek(0, SeekOrigin.Begin); + await stream.CopyToAsync(streamToWriteTo); + streamToWriteTo.Flush(); + filesize = streamToWriteTo.Length; + } + + dataElement.Size = filesize; + WriteDataElementToFile(dataElement, org, app, instanceOwnerId); + + return dataElement; + } + + public Task UpdateBinaryData(InstanceIdentifier instanceIdentifier, string? contentType, string filename, Guid dataGuid, Stream stream) + { + throw new NotImplementedException(); + } + + public async Task InsertBinaryData(string instanceId, string dataType, string contentType, string filename, Stream stream, string? generatedFromTask = null) + { + Application application = await _appMetadata.GetApplicationMetadata(); + var instanceIdParts = instanceId.Split("/"); + + Guid dataGuid = Guid.NewGuid(); + + string org = application.Org; + string app = application.Id.Split("/")[1]; + int instanceOwnerId = int.Parse(instanceIdParts[0]); + Guid instanceGuid = Guid.Parse(instanceIdParts[1]); + + string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerId, instanceGuid); + + DataElement dataElement = new() { Id = dataGuid.ToString(), InstanceGuid = instanceGuid.ToString(), DataType = dataType, ContentType = contentType, }; + + if (!Directory.Exists(Path.GetDirectoryName(dataPath))) + { + var directory = Path.GetDirectoryName(dataPath); + if (directory != null) Directory.CreateDirectory(directory); + } + + Directory.CreateDirectory(dataPath + @"blob"); + + long filesize; + + using (Stream streamToWriteTo = File.Open(dataPath + @"blob/" + dataGuid, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + stream.Seek(0, SeekOrigin.Begin); + await stream.CopyToAsync(streamToWriteTo); + streamToWriteTo.Flush(); + filesize = streamToWriteTo.Length; + } + + dataElement.Size = filesize; + WriteDataElementToFile(dataElement, org, app, instanceOwnerId); + + return dataElement; + } + + public Task UpdateBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid, HttpRequest request) + { + throw new NotImplementedException(); + } + + public Task Update(Instance instance, DataElement dataElement) + { + string org = instance.Org; + string app = instance.AppId.Split("/")[1]; + int instanceOwnerId = int.Parse(instance.InstanceOwner.PartyId); + + WriteDataElementToFile(dataElement, org, app, instanceOwnerId); + + return Task.FromResult(dataElement); + } + + public Task LockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + { + throw new NotImplementedException(); + } + + public Task UnlockDataElement(InstanceIdentifier instanceIdentifier, Guid dataGuid) + { + throw new NotImplementedException(); + } + + private static void WriteDataElementToFile(DataElement dataElement, string org, string app, int instanceOwnerPartyId) + { + string dataElementPath = TestData.GetDataElementPath(org, app, instanceOwnerPartyId, Guid.Parse(dataElement.InstanceGuid), Guid.Parse(dataElement.Id)); + + string jsonData = JsonSerializer.Serialize(dataElement, _serializerOptions); + + using StreamWriter sw = new(dataElementPath); + + sw.Write(jsonData.ToString()); + sw.Close(); + } + + private List GetDataElements(string org, string app, int instanceOwnerId, Guid instanceId) + { + string path = TestData.GetDataDirectory(org, app, instanceOwnerId, instanceId); + List dataElements = new(); + + if (!Directory.Exists(path)) + { + return new List(); + } + + string[] files = Directory.GetFiles(path); + + foreach (string file in files) + { + string content = File.ReadAllText(Path.Combine(path, file)); + DataElement? dataElement = JsonSerializer.Deserialize(content, _serializerOptions); + + if (dataElement != null) + { + if (dataElement.DeleteStatus?.IsHardDeleted == true && string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.User.GetOrg())) + { + continue; + } + + dataElements.Add(dataElement); + } + } + + return dataElements; + } + } +} diff --git a/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs index e4d8c0ede..99dc9d0e2 100644 --- a/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs +++ b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs @@ -130,7 +130,7 @@ private static Instance GetTestInstance(string app, string org, int instanceOwne // Finds the path for the instance based on instanceId. Only works if guid is unique. private static string GetInstancePath(int instanceOwnerPartyId, Guid instanceGuid) { - string[] paths = Directory.GetFiles(TestData.GetTestDataInstancesFolder(), instanceGuid + ".json", SearchOption.AllDirectories); + string[] paths = Directory.GetFiles(TestData.GetInstancesDirectory(), instanceGuid + ".json", SearchOption.AllDirectories); paths = paths.Where(p => p.Contains($"{instanceOwnerPartyId}")).ToArray(); if (paths.Length == 1) { @@ -142,24 +142,25 @@ private static string GetInstancePath(int instanceOwnerPartyId, Guid instanceGui private static string GetInstancePath(string app, string org, int instanceOwnerId, Guid instanceId) { - return Path.Combine(TestData.GetTestDataInstancesFolder(), org, app, instanceOwnerId.ToString(), instanceId.ToString() + ".json"); + return Path.Combine(TestData.GetInstancesDirectory(), org, app, instanceOwnerId.ToString(), instanceId + ".json"); } private static string GetDataPath(string org, string app, int instanceOwnerId, Guid instanceGuid) { - return Path.Combine(TestData.GetTestDataInstancesFolder(), org, app, instanceOwnerId.ToString(), instanceGuid.ToString()) + Path.DirectorySeparatorChar; + return Path.Combine(TestData.GetInstancesDirectory(), org, app, instanceOwnerId.ToString(), instanceGuid.ToString()) + Path.DirectorySeparatorChar; } private List GetDataElements(string org, string app, int instanceOwnerId, Guid instanceId) { string path = GetDataPath(org, app, instanceOwnerId, instanceId); - + + List dataElements = new(); + if (!Directory.Exists(path)) { - throw new IOException($"Can't find data path {path} for instance {instanceId} in app {org}/{app}"); + return dataElements; } - - List dataElements = new(); + foreach (string file in Directory.GetFiles(path)) { if (file.Contains(".pretest")) @@ -412,13 +413,13 @@ public async Task> GetInstances(Dictionary List instances = new(); - string instancesPath = TestData.GetTestDataInstancesFolder(); + string instancesPath = TestData.GetInstancesDirectory(); int fileDepth = 4; if (queryParams.TryGetValue("appId", out StringValues appIdQueryVal) && appIdQueryVal.Count > 0) { - instancesPath += Path.DirectorySeparatorChar + appIdQueryVal.First().Replace('/', Path.DirectorySeparatorChar); + instancesPath += Path.DirectorySeparatorChar + appIdQueryVal.First()?.Replace('/', Path.DirectorySeparatorChar); fileDepth -= 2; if (queryParams.TryGetValue("instanceOwner.partyId", out StringValues partyIdQueryVal) && partyIdQueryVal.Count > 0) diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs index b28c7ccac..1b6fa9e3f 100644 --- a/test/Altinn.App.Api.Tests/Program.cs +++ b/test/Altinn.App.Api.Tests/Program.cs @@ -4,10 +4,13 @@ using Altinn.App.Api.Tests.Mocks.Event; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; +using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Internal.Instances; using AltinnCore.Authentication.JwtCookie; +using App.IntegrationTests.Mocks.Services; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -47,6 +50,8 @@ void ConfigureMockServices(IServiceCollection services, ConfigurationManager con services.AddSingleton(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } void Configure() diff --git a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs index 81c5aff61..2a86d6c77 100644 --- a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs +++ b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs @@ -70,5 +70,22 @@ public static string GetSelfIdentifiedUserToken( return token; } + + public static string GetOrgToken(string org, string orgNo, int authenticationLevel = 4) + { + List claims = new List(); + string issuer = "www.altinn.no"; + claims.Add(new Claim(AltinnCoreClaimTypes.Org, org, ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.OrgNumber, orgNo, ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticationLevel, authenticationLevel.ToString(), ClaimValueTypes.Integer32, issuer)); + + ClaimsIdentity identity = new ClaimsIdentity("mock"); + identity.AddClaims(claims); + ClaimsPrincipal principal = new ClaimsPrincipal(identity); + string token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); + + return token; + } } } diff --git a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj index 5ee568f06..a1f8198db 100644 --- a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj +++ b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj @@ -19,13 +19,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index 9051197e7..70a00ed5a 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -31,21 +31,21 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers diff --git a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs index 298817399..552e47845 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features.Action; using Altinn.App.Core.Helpers; @@ -15,6 +14,7 @@ using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Microsoft.FeatureManagement; using Moq; using Xunit; using Signee = Altinn.App.Core.Internal.Sign.Signee; @@ -118,7 +118,7 @@ private static (SigningUserAction SigningUserAction, Mock SignClien ApplicationMetadataFileName = "appmetadata.json" }; - IAppMetadata appMetadata = new AppMetadata(Options.Create(appSettings), new FrontendFeatures()); + IAppMetadata appMetadata = new AppMetadata(Options.Create(appSettings), new FrontendFeatures(new Mock().Object)); var profileClientMock = new Mock(); var signingClientMock = new Mock(); profileClientMock.Setup(p => p.GetUserProfile(It.IsAny())).ReturnsAsync(userProfileToReturn); diff --git a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs index e3066e229..8040432b3 100644 --- a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs @@ -1,10 +1,12 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Implementation; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Microsoft.FeatureManagement; using Moq; using Xunit; @@ -55,7 +57,7 @@ public void GetApplication_desrializes_file_from_disk() Person = true, SubUnit = true }, - OnEntry = new OnEntryConfig() + OnEntry = new OnEntry() { Show = "select-instance" } @@ -64,6 +66,53 @@ public void GetApplication_desrializes_file_from_disk() actual.Should().NotBeNull(); actual.Should().BeEquivalentTo(expected); } + + [Fact] + public void GetApplication_handles_onEntry_null() + { + AppSettings appSettings = GetAppSettings("AppMetadata", "no-on-entry.applicationmetadata.json"); + var settings = Options.Create(appSettings); + IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); + IAppResources appResources = new AppResourcesSI(settings, appMetadata, null, new NullLogger()); + Application expected = new Application() + { + Id = "tdd/bestilling", + Org = "tdd", + Created = DateTime.Parse("2019-09-16T22:22:22"), + CreatedBy = "username", + Title = new Dictionary() + { + { "nb", "Bestillingseksempelapp" } + }, + DataTypes = new List() + { + new() + { + Id = "vedlegg", + AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, + MinCount = 0, + TaskId = "Task_1" + }, + new() + { + Id = "ref-data-as-pdf", + AllowedContentTypes = new List() { "application/pdf" }, + MinCount = 1, + TaskId = "Task_1" + } + }, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true + } + }; + var actual = appResources.GetApplication(); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); + } [Fact] public void GetApplication_second_read_from_cache() @@ -108,7 +157,7 @@ public void GetApplication_second_read_from_cache() Person = true, SubUnit = true }, - OnEntry = new OnEntryConfig() + OnEntry = new OnEntry() { Show = "select-instance" }, @@ -205,9 +254,11 @@ private AppSettings GetAppSettings(string subfolder, string appMetadataFilename private static IAppMetadata SetupAppMedata(IOptions appsettings, IFrontendFeatures frontendFeatures = null) { + var featureManagerMock = new Mock(); + if (frontendFeatures == null) { - return new AppMetadata(appsettings, new FrontendFeatures()); + return new AppMetadata(appsettings, new FrontendFeatures(featureManagerMock.Object)); } return new AppMetadata(appsettings, frontendFeatures); diff --git a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs b/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs index a6619c45f..bec09e046 100644 --- a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs @@ -637,7 +637,7 @@ private ApplicationMetadata GetApplicationMetadataForShadowFields(bool useSaveTo Person = true, SubUnit = true }, - OnEntry = new OnEntryConfig() + OnEntry = new OnEntry() { Show = "select-instance" } diff --git a/test/Altinn.App.Core.Tests/Implementation/TestData/AppMetadata/no-on-entry.applicationmetadata.json b/test/Altinn.App.Core.Tests/Implementation/TestData/AppMetadata/no-on-entry.applicationmetadata.json new file mode 100644 index 000000000..5bcd6c2ce --- /dev/null +++ b/test/Altinn.App.Core.Tests/Implementation/TestData/AppMetadata/no-on-entry.applicationmetadata.json @@ -0,0 +1,27 @@ +{ + "id": "tdd/bestilling", + "org": "tdd", + "created": "2019-09-16T22:22:22", + "createdBy": "username", + "title": { "nb": "Bestillingseksempelapp" }, + "dataTypes": [ + { + "id": "vedlegg", + "allowedContentTypes": [ "application/pdf", "image/png", "image/jpeg" ], + "minCount": 0, + "taskId": "Task_1", + }, + { + "id": "ref-data-as-pdf", + "allowedContentTypes": [ "application/pdf" ], + "minCount": 1, + "taskId": "Task_1", + } + ], + "partyTypesAllowed": { + "bankruptcyEstate": true, + "organisation": true, + "person": true, + "subUnit": true + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs index 1a88da473..3c4a9a352 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs @@ -4,6 +4,7 @@ using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.Extensions.Options; +using Microsoft.FeatureManagement; using Moq; using Xunit; @@ -14,8 +15,12 @@ public class AppMedataTest private readonly string appBasePath = Path.Combine("Internal", "App", "TestData") + Path.DirectorySeparatorChar; [Fact] - public async void GetApplicationMetadata_desrializes_file_from_disk() + public async Task GetApplicationMetadata_desrializes_file_from_disk() { + var featureManagerMock = new Mock(); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); + AppSettings appSettings = GetAppSettings("AppMetadata", "default.applicationmetadata.json"); IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") @@ -52,15 +57,11 @@ public async void GetApplicationMetadata_desrializes_file_from_disk() Person = true, SubUnit = true }, - OnEntry = new OnEntryConfig() + OnEntry = new OnEntry() { Show = "select-instance" }, - Features = new Dictionary() - { - { "footer", true }, - { "processActions", true } - } + Features = enabledFrontendFeatures }; var actual = await appMetadata.GetApplicationMetadata(); actual.Should().NotBeNull(); @@ -68,8 +69,12 @@ public async void GetApplicationMetadata_desrializes_file_from_disk() } [Fact] - public async void GetApplicationMetadata_eformidling_desrializes_file_from_disk() + public async Task GetApplicationMetadata_eformidling_desrializes_file_from_disk() { + var featureManagerMock = new Mock(); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); + AppSettings appSettings = GetAppSettings("AppMetadata", "eformid.applicationmetadata.json"); IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") @@ -122,15 +127,11 @@ public async void GetApplicationMetadata_eformidling_desrializes_file_from_disk( "372c7af5-71e1-4e99-8e05-4716711a8b53", } }, - OnEntry = new OnEntryConfig() + OnEntry = new OnEntry() { Show = "select-instance" }, - Features = new Dictionary() - { - { "footer", true }, - { "processActions", true } - } + Features = enabledFrontendFeatures }; var actual = await appMetadata.GetApplicationMetadata(); actual.Should().NotBeNull(); @@ -178,7 +179,7 @@ public async void GetApplicationMetadata_second_read_from_cache() Person = true, SubUnit = true }, - OnEntry = new OnEntryConfig() + OnEntry = new OnEntry() { Show = "select-instance" }, @@ -196,6 +197,200 @@ public async void GetApplicationMetadata_second_read_from_cache() actual2.Should().BeEquivalentTo(expected); } + [Fact] + public async Task GetApplicationMetadata_onEntry_InstanceSelection_DefaultSelectedOption_read_legacy_value_if_new_not_set() + { + var featureManagerMock = new Mock(); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); + + AppSettings appSettings = GetAppSettings("AppMetadata", "onentry-legacy-selectoptions.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); + ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + { + Id = "tdd/bestilling", + Org = "tdd", + Created = DateTime.Parse("2019-09-16T22:22:22"), + CreatedBy = "username", + Title = new Dictionary() + { + { "nb", "Bestillingseksempelapp" } + }, + DataTypes = new List() + { + new() + { + Id = "vedlegg", + AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, + MinCount = 0, + TaskId = "Task_1" + }, + new() + { + Id = "ref-data-as-pdf", + AllowedContentTypes = new List() { "application/pdf" }, + MinCount = 1, + TaskId = "Task_1" + } + }, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true + }, + OnEntry = new OnEntry() + { + Show = "select-instance", + InstanceSelection = new() + { + SortDirection = "desc", + RowsPerPageOptions = new List() + { + 5, 3, 10, 25, 50, 100 + }, + DefaultRowsPerPage = 1, + DefaultSelectedOption = 1 + } + }, + Features = enabledFrontendFeatures + }; + var actual = await appMetadata.GetApplicationMetadata(); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); + actual.OnEntry?.InstanceSelection?.DefaultSelectedOption.Should().Be(1); + } + + [Fact] + public async Task GetApplicationMetadata_onEntry_supports_new_option() + { + var featureManagerMock = new Mock(); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); + + AppSettings appSettings = GetAppSettings("AppMetadata", "onentry-new-selectoptions.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); + ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + { + Id = "tdd/bestilling", + Org = "tdd", + Created = DateTime.Parse("2019-09-16T22:22:22"), + CreatedBy = "username", + Title = new Dictionary() + { + { "nb", "Bestillingseksempelapp" } + }, + DataTypes = new List() + { + new() + { + Id = "vedlegg", + AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, + MinCount = 0, + TaskId = "Task_1" + }, + new() + { + Id = "ref-data-as-pdf", + AllowedContentTypes = new List() { "application/pdf" }, + MinCount = 1, + TaskId = "Task_1" + } + }, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true + }, + OnEntry = new OnEntry() + { + Show = "select-instance", + InstanceSelection = new() + { + SortDirection = "desc", + RowsPerPageOptions = new List() + { + 5, 3, 10, 25, 50, 100 + }, + DefaultSelectedOption = 2 + } + }, + Features = enabledFrontendFeatures + }; + var actual = await appMetadata.GetApplicationMetadata(); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); + actual.OnEntry?.InstanceSelection?.DefaultSelectedOption.Should().Be(2); + } + + [Fact] + public async Task GetApplicationMetadata_onEntry_prefer_new_option() + { + var featureManagerMock = new Mock(); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + Dictionary enabledFrontendFeatures = await frontendFeatures.GetFrontendFeatures(); + + AppSettings appSettings = GetAppSettings("AppMetadata", "onentry-prefer-new-selectoptions.applicationmetadata.json"); + IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); + ApplicationMetadata expected = new ApplicationMetadata("tdd/bestilling") + { + Id = "tdd/bestilling", + Org = "tdd", + Created = DateTime.Parse("2019-09-16T22:22:22"), + CreatedBy = "username", + Title = new Dictionary() + { + { "nb", "Bestillingseksempelapp" } + }, + DataTypes = new List() + { + new() + { + Id = "vedlegg", + AllowedContentTypes = new List() { "application/pdf", "image/png", "image/jpeg" }, + MinCount = 0, + TaskId = "Task_1" + }, + new() + { + Id = "ref-data-as-pdf", + AllowedContentTypes = new List() { "application/pdf" }, + MinCount = 1, + TaskId = "Task_1" + } + }, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true + }, + OnEntry = new OnEntry() + { + Show = "select-instance", + InstanceSelection = new() + { + SortDirection = "desc", + RowsPerPageOptions = new List() + { + 5, 3, 10, 25, 50, 100 + }, + DefaultRowsPerPage = 1, + DefaultSelectedOption = 3 + } + }, + Features = enabledFrontendFeatures + }; + var actual = await appMetadata.GetApplicationMetadata(); + actual.Should().NotBeNull(); + actual.Should().BeEquivalentTo(expected); + actual.OnEntry?.InstanceSelection?.DefaultSelectedOption.Should().Be(3); + } + [Fact] public async void GetApplicationMetadata_throws_ApplicationConfigException_if_file_not_found() { @@ -273,11 +468,13 @@ private AppSettings GetAppSettings(string subfolder, string appMetadataFilename private static IAppMetadata SetupAppMedata(IOptions appsettings, IFrontendFeatures frontendFeatures = null) { + var featureManagerMock = new Mock(); + if (frontendFeatures == null) { - return new AppMetadata(appsettings, new FrontendFeatures()); + return new AppMetadata(appsettings, new FrontendFeatures(featureManagerMock.Object)); } - + return new AppMetadata(appsettings, frontendFeatures); } } diff --git a/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs b/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs index e97c1e619..c34a386c2 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs @@ -1,5 +1,8 @@ +using Altinn.App.Core.Features; using Altinn.App.Core.Internal.App; using FluentAssertions; +using Microsoft.FeatureManagement; +using Moq; using Xunit; namespace Altinn.App.Core.Tests.Internal.App @@ -7,14 +10,34 @@ namespace Altinn.App.Core.Tests.Internal.App public class FrontendFeaturesTest { [Fact] - public async void GetFeatures_returns_list_of_enabled_features() + public async Task GetFeatures_returns_list_of_enabled_features() { Dictionary expected = new Dictionary() { { "footer", true }, { "processActions", true }, + { "jsonObjectInDataResponse", false }, }; - IFrontendFeatures frontendFeatures = new FrontendFeatures(); + var featureManagerMock = new Mock(); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); + + var actual = await frontendFeatures.GetFrontendFeatures(); + + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task GetFeatures_returns_list_of_enabled_features_when_feature_flag_is_enabled() + { + Dictionary expected = new Dictionary() + { + { "footer", true }, + { "processActions", true }, + { "jsonObjectInDataResponse", true }, + }; + var featureManagerMock = new Mock(); + featureManagerMock.Setup(f => f.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse, default)).ReturnsAsync(true); + IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); var actual = await frontendFeatures.GetFrontendFeatures(); actual.Should().BeEquivalentTo(expected); } diff --git a/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/onentry-legacy-selectoptions.applicationmetadata.json b/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/onentry-legacy-selectoptions.applicationmetadata.json new file mode 100644 index 000000000..16b5742fb --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/onentry-legacy-selectoptions.applicationmetadata.json @@ -0,0 +1,35 @@ +{ + "id": "tdd/bestilling", + "org": "tdd", + "created": "2019-09-16T22:22:22", + "createdBy": "username", + "title": { "nb": "Bestillingseksempelapp" }, + "dataTypes": [ + { + "id": "vedlegg", + "allowedContentTypes": [ "application/pdf", "image/png", "image/jpeg" ], + "minCount": 0, + "taskId": "Task_1", + }, + { + "id": "ref-data-as-pdf", + "allowedContentTypes": [ "application/pdf" ], + "minCount": 1, + "taskId": "Task_1", + } + ], + "partyTypesAllowed": { + "bankruptcyEstate": true, + "organisation": true, + "person": true, + "subUnit": true + }, + "onEntry": { + "show": "select-instance", + "instanceSelection": { + "sortDirection": "desc", + "rowsPerPageOptions": [5, 3, 10, 25, 50, 100], + "defaultRowsPerPage": 1 + } + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/onentry-new-selectoptions.applicationmetadata.json b/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/onentry-new-selectoptions.applicationmetadata.json new file mode 100644 index 000000000..68411e9ad --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/onentry-new-selectoptions.applicationmetadata.json @@ -0,0 +1,35 @@ +{ + "id": "tdd/bestilling", + "org": "tdd", + "created": "2019-09-16T22:22:22", + "createdBy": "username", + "title": { "nb": "Bestillingseksempelapp" }, + "dataTypes": [ + { + "id": "vedlegg", + "allowedContentTypes": [ "application/pdf", "image/png", "image/jpeg" ], + "minCount": 0, + "taskId": "Task_1", + }, + { + "id": "ref-data-as-pdf", + "allowedContentTypes": [ "application/pdf" ], + "minCount": 1, + "taskId": "Task_1", + } + ], + "partyTypesAllowed": { + "bankruptcyEstate": true, + "organisation": true, + "person": true, + "subUnit": true + }, + "onEntry": { + "show": "select-instance", + "instanceSelection": { + "sortDirection": "desc", + "rowsPerPageOptions": [5, 3, 10, 25, 50, 100], + "defaultSelectedOption": 2 + } + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/onentry-prefer-new-selectoptions.applicationmetadata.json b/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/onentry-prefer-new-selectoptions.applicationmetadata.json new file mode 100644 index 000000000..3a66d83dd --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/onentry-prefer-new-selectoptions.applicationmetadata.json @@ -0,0 +1,36 @@ +{ + "id": "tdd/bestilling", + "org": "tdd", + "created": "2019-09-16T22:22:22", + "createdBy": "username", + "title": { "nb": "Bestillingseksempelapp" }, + "dataTypes": [ + { + "id": "vedlegg", + "allowedContentTypes": [ "application/pdf", "image/png", "image/jpeg" ], + "minCount": 0, + "taskId": "Task_1", + }, + { + "id": "ref-data-as-pdf", + "allowedContentTypes": [ "application/pdf" ], + "minCount": 1, + "taskId": "Task_1", + } + ], + "partyTypesAllowed": { + "bankruptcyEstate": true, + "organisation": true, + "person": true, + "subUnit": true + }, + "onEntry": { + "show": "select-instance", + "instanceSelection": { + "sortDirection": "desc", + "rowsPerPageOptions": [5, 3, 10, 25, 50, 100], + "defaultRowsPerPage": 1, + "defaultSelectedOption": 3 + } + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/TestDoubles/Skjema.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/TestDoubles/Skjema.cs index 41a970d8b..e43f45212 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/TestDoubles/Skjema.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/TestDoubles/Skjema.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; namespace Altinn.App.Core.Internal.Pdf.TestDoubles; + public class Skjema { [XmlElement("melding", Order = 1)] diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs index 4e4d0dc91..3fbd7722e 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs @@ -31,15 +31,27 @@ public TestFunctions(ITestOutputHelper output) [Theory] [SharedTest("component")] public void Component_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + + [Theory] + [SharedTest("commaContains")] + public void CommaContains_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); [Theory] [SharedTest("concat")] public void Concat_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + [Theory] + [SharedTest("contains")] + public void Contains_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + [Theory] [SharedTest("dataModel")] public void DataModel_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + [Theory] + [SharedTest("endsWith")] + public void EndsWith_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + [Theory] [SharedTest("equals")] public void Equals_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); @@ -59,6 +71,10 @@ public TestFunctions(ITestOutputHelper output) [Theory] [SharedTest("not")] public void Not_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + + [Theory] + [SharedTest("notContains")] + public void NotContains_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); [Theory] [SharedTest("instanceContext")] @@ -83,6 +99,26 @@ public TestFunctions(ITestOutputHelper output) [Theory] [SharedTest("unknown")] public void Unknown_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + + [Theory] + [SharedTest("upperCase")] + public void UpperCase_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + + [Theory] + [SharedTest("lowerCase")] + public void LowerCase_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + + [Theory] + [SharedTest("startsWith")] + public void StartsWith_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + + [Theory] + [SharedTest("stringLength")] + public void StringLength_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); + + [Theory] + [SharedTest("round")] + public void Round_Theory(ExpressionTestCaseRoot test) => RunTestCase(test); private void RunTestCase(ExpressionTestCaseRoot test) { diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/noData.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/noData.json index 360768a36..1596105a1 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/noData.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/noData.json @@ -6,7 +6,7 @@ "layout": [ { "id": "comp1", - "type": "Heading" + "type": "Header" }, { "id": "group1", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/oneRow.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/oneRow.json index 26ba4e5d6..b043d9411 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/oneRow.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/oneRow.json @@ -6,7 +6,7 @@ "layout": [ { "id": "comp1", - "type": "Heading" + "type": "Header" }, { "id": "group1", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/twoRows.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/twoRows.json index efcd93c0d..89dd4f259 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/twoRows.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/twoRows.json @@ -6,7 +6,7 @@ "layout": [ { "id": "comp1", - "type": "Heading" + "type": "Header" }, { "id": "group1", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount0.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount0.json index 8db04ce09..8c038be56 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount0.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount0.json @@ -6,7 +6,7 @@ "layout": [ { "id": "comp1", - "type": "Heading" + "type": "Header" }, { "id": "group1", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount1.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount1.json index 06199128e..7019c6eed 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount1.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount1.json @@ -6,7 +6,7 @@ "layout": [ { "id": "comp1", - "type": "Heading" + "type": "Header" }, { "id": "group1", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/simple.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/simple.json index 352a1711f..56d2c9252 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/simple.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/simple.json @@ -6,7 +6,7 @@ "layout": [ { "id": "comp1", - "type": "Heading" + "type": "Header" }, { "id": "group1", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveNoData.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveNoData.json index e1f37b526..05ff77bc9 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveNoData.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveNoData.json @@ -6,7 +6,7 @@ "layout": [ { "id": "comp1", - "type": "Heading" + "type": "Header" }, { "id": "group0", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveOneRow.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveOneRow.json index eb995da76..a0329b437 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveOneRow.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveOneRow.json @@ -6,7 +6,7 @@ "layout": [ { "id": "comp1", - "type": "Heading" + "type": "Header" }, { "id": "group0", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsInner.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsInner.json index 431fbf231..d4b60ccd3 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsInner.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsInner.json @@ -6,7 +6,7 @@ "layout": [ { "id": "comp1", - "type": "Heading" + "type": "Header" }, { "id": "group0", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsOuter.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsOuter.json index 652d1df29..0bf29ee1c 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsOuter.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsOuter.json @@ -6,7 +6,7 @@ "layout": [ { "id": "comp1", - "type": "Heading" + "type": "Header" }, { "id": "group0", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/simple/twoPages.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/simple/twoPages.json index bbe977f53..74356d8de 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/simple/twoPages.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/simple/twoPages.json @@ -6,7 +6,7 @@ "layout": [ { "id": "comp1", - "type": "Heading" + "type": "Header" }, { "id": "comp2", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/empty-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/empty-string.json new file mode 100644 index 000000000..5beadc3d5 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/empty-string.json @@ -0,0 +1,5 @@ +{ + "name": "Should not contain an empty string", + "expression": ["commaContains", "40, 50, 60", ""], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/null.json new file mode 100644 index 000000000..30e4156c5 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/null.json @@ -0,0 +1,5 @@ +{ + "name": "Should not contain a null value", + "expression": ["commaContains", "40, 50, 60", null], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/null2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/null2.json new file mode 100644 index 000000000..009bae254 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/null2.json @@ -0,0 +1,5 @@ +{ + "name": "Should not split a null value", + "expression": ["commaContains", null, "hello world"], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/should-include-word-in-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/should-include-word-in-string.json new file mode 100644 index 000000000..0f4a9c0e0 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/should-include-word-in-string.json @@ -0,0 +1,5 @@ +{ + "name": "Should return true if the comma-separated list contains the given value", + "expression": ["commaContains", "hello, bye, hola, adios", "hola"], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/should-not-include-word-in-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/should-not-include-word-in-string.json new file mode 100644 index 000000000..a2af3ad5a --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/should-not-include-word-in-string.json @@ -0,0 +1,5 @@ +{ + "name": "Should return false if the comma-separated list does not contain the given value", + "expression": ["commaContains", "hello, bye, hola, adios", "Hasta luego"], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/string-list-include-number.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/string-list-include-number.json new file mode 100644 index 000000000..f4049e394 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/commaContains/string-list-include-number.json @@ -0,0 +1,5 @@ +{ + "name": "Should return true if the comma-separated list contains the given value even if its provided as a number", + "expression": ["commaContains", "40, 50, 60", 40], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/distant-across-page.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/distant-across-page.json index 9e1aa5b06..c6beab899 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/distant-across-page.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/distant-across-page.json @@ -1,45 +1,45 @@ { - "name": "Lookup component that cannot be found using closest() across page", - "expression": ["component", "navn"], - "expects": "Kaptein Sabeltann", - "context": { - "component": "information", - "currentLayout": "Summary" - }, - "dataModel": { - "Navn": "Kaptein Sabeltann" - }, - "layouts": { - "Form": { - "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", - "data": { - "layout": [ - { - "id": "group", - "type": "Group", - "dataModelBindings": {}, - "children": ["navn"] - }, - { - "id": "navn", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "Navn" - } - } - ] - } - }, - "Summary": { - "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", - "data": { - "layout": [ - { - "id": "information", - "type": "Panel" + "name": "Lookup component that cannot be found using closest() across page", + "expression": ["component", "navn"], + "expects": "Kaptein Sabeltann", + "context": { + "component": "information", + "currentLayout": "Summary" + }, + "dataModel": { + "Navn": "Kaptein Sabeltann" + }, + "layouts": { + "Form": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "group", + "type": "Group", + "dataModelBindings": {}, + "children": ["navn"] + }, + { + "id": "navn", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "Navn" } - ] - } + } + ] + } + }, + "Summary": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "information", + "type": "Panel" + } + ] } } - } \ No newline at end of file + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/distant.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/distant.json index 109ab57cd..2ad95271d 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/distant.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/distant.json @@ -1,38 +1,38 @@ { - "name": "Lookup component that cannot be found using closest()", - "expression": ["component", "navn"], - "expects": "Kaptein Sabeltann", - "context": { - "component": "information", - "currentLayout": "Form" - }, - "dataModel": { - "Navn": "Kaptein Sabeltann" - }, - "layouts": { - "Form": { - "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", - "data": { - "layout": [ - { - "id": "group", - "type": "Group", - "dataModelBindings": {}, - "children": ["navn"] - }, - { - "id": "navn", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "Navn" - } - }, - { - "id": "information", - "type": "Panel" + "name": "Lookup component that cannot be found using closest()", + "expression": ["component", "navn"], + "expects": "Kaptein Sabeltann", + "context": { + "component": "information", + "currentLayout": "Form" + }, + "dataModel": { + "Navn": "Kaptein Sabeltann" + }, + "layouts": { + "Form": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "group", + "type": "Group", + "dataModelBindings": {}, + "children": ["navn"] + }, + { + "id": "navn", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "Navn" } - ] - } + }, + { + "id": "information", + "type": "Panel" + } + ] } } - } \ No newline at end of file + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/case-sensitivity.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/case-sensitivity.json new file mode 100644 index 000000000..e761b184a --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/case-sensitivity.json @@ -0,0 +1,5 @@ +{ + "name": "Should not match case sensitive", + "expression": ["contains", "Hello", "hello"], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/empty-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/empty-string.json new file mode 100644 index 000000000..b4e14894f --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/empty-string.json @@ -0,0 +1,5 @@ +{ + "name": "Should always contain en empty string", + "expression": ["contains", "Hello", ""], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/exact-match.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/exact-match.json new file mode 100644 index 000000000..5f9667e20 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/exact-match.json @@ -0,0 +1,5 @@ +{ + "name": "Should return true if the first string contains the second string", + "expression": ["contains", "abc", "abc"], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/not-match.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/not-match.json new file mode 100644 index 000000000..1c4d59214 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/not-match.json @@ -0,0 +1,5 @@ +{ + "name": "should return false if the second string does not contain the first string", + "expression": ["contains", "Hello", "Bye"], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null.json new file mode 100644 index 000000000..f53527bde --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null.json @@ -0,0 +1,5 @@ +{ + "name": "Should always return false when the input is null", + "expression": ["contains", null, "null"], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null2.json new file mode 100644 index 000000000..0302631b7 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null2.json @@ -0,0 +1,5 @@ +{ + "name": "Should always return false when the input is null", + "expression": ["contains", "null", null], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null3.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null3.json new file mode 100644 index 000000000..b525fb777 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null3.json @@ -0,0 +1,5 @@ +{ + "name": "Should always return false when the input is null", + "expression": ["contains", null, null], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null4.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null4.json new file mode 100644 index 000000000..286a2e3cf --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/null4.json @@ -0,0 +1,5 @@ +{ + "name": "Should treat stringy nulls as null", + "expression": ["contains", "null", "null"], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/partial-match.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/partial-match.json new file mode 100644 index 000000000..1dc17e579 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/contains/partial-match.json @@ -0,0 +1,5 @@ +{ + "name": "Should return true if the string contains the substring", + "expression": ["contains", "abc", "b"], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/case-sensitivity.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/case-sensitivity.json new file mode 100644 index 000000000..6e63b5b21 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/case-sensitivity.json @@ -0,0 +1,5 @@ +{ + "name": "Should be case sensitive and return false if the string does end with the given string but opposite case", + "expression": ["endsWith", "Hello", "LO"], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/empty-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/empty-string.json new file mode 100644 index 000000000..1626ffef4 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/empty-string.json @@ -0,0 +1,5 @@ +{ + "name": "All strings ends with an empty string", + "expression": ["endsWith", "Hello", ""], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/ends-with-null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/ends-with-null.json new file mode 100644 index 000000000..59e6f17eb --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/ends-with-null.json @@ -0,0 +1,5 @@ +{ + "name": "No string ends with a null", + "expression": ["endsWith", "Hello", null], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/ends-with-number.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/ends-with-number.json new file mode 100644 index 000000000..e401cac5c --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/ends-with-number.json @@ -0,0 +1,5 @@ +{ + "name": "Should be true when the string ends with the given string (even when the given 'string' is a number)", + "expression": ["endsWith", "Im 40", 40], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/ends-with.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/ends-with.json new file mode 100644 index 000000000..37fcd96e9 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/ends-with.json @@ -0,0 +1,5 @@ +{ + "name": "Should be true when the string ends with the specified string", + "expression": ["endsWith", "Hello", "lo"], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/exact-match.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/exact-match.json new file mode 100644 index 000000000..1c9ced8af --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/exact-match.json @@ -0,0 +1,5 @@ +{ + "name": "Should be true when the string ends with the specified string, even on exact match", + "expression": ["endsWith", "Hello", "Hello"], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/not-ends-with.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/not-ends-with.json new file mode 100644 index 000000000..fbf25b86c --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/not-ends-with.json @@ -0,0 +1,5 @@ +{ + "name": "Should be false when the string does not ends with the specified string", + "expression": ["endsWith", "Hello", "me"], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/number-ends-with-number.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/number-ends-with-number.json new file mode 100644 index 000000000..1b2bc1b7c --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/endsWith/number-ends-with-number.json @@ -0,0 +1,5 @@ +{ + "name": "Should be true when the given number ends with the given digit", + "expression": ["endsWith", 102, 2], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/lowercase-number-should-return-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/lowercase-number-should-return-string.json new file mode 100644 index 000000000..7b6e3a1df --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/lowercase-number-should-return-string.json @@ -0,0 +1,5 @@ +{ + "name": "Should return number as string when number is passed", + "expression": ["lowerCase", 40], + "expects": "40" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/null.json new file mode 100644 index 000000000..d341f9767 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/null.json @@ -0,0 +1,5 @@ +{ + "name": "Should return a null when the input is a null", + "expression": ["lowerCase", null], + "expects": null +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/should-lowercase-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/should-lowercase-string.json new file mode 100644 index 000000000..5f38166d8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/should-lowercase-string.json @@ -0,0 +1,5 @@ +{ + "name": "Should lowercase the string", + "expression": ["lowerCase", "HELLO"], + "expects": "hello" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/should-lowercase-whole-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/should-lowercase-whole-string.json new file mode 100644 index 000000000..7a90a8d22 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/should-lowercase-whole-string.json @@ -0,0 +1,5 @@ +{ + "name": "Should lowercase the whole string when given a string has multiple words", + "expression": ["lowerCase", "Hola, Como Estas?"], + "expects": "hola, como estas?" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/should-lowercase-whole-word.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/should-lowercase-whole-word.json new file mode 100644 index 000000000..4b9fa972a --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/lowerCase/should-lowercase-whole-word.json @@ -0,0 +1,5 @@ +{ + "name": "Should lowercase the whole string", + "expression": ["lowerCase", "HElLo"], + "expects": "hello" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/case-sensitivity.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/case-sensitivity.json new file mode 100644 index 000000000..0cdf815c0 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/case-sensitivity.json @@ -0,0 +1,5 @@ +{ + "name": "Should not match case sensitive", + "expression": ["notContains", "Hello", "hello"], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/exact-match.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/exact-match.json new file mode 100644 index 000000000..02c5f9b68 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/exact-match.json @@ -0,0 +1,5 @@ +{ + "name": "Should return false when the string does contain the substring", + "expression": ["notContains", "abc", "abc"], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/not-match.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/not-match.json new file mode 100644 index 000000000..8cc79e6f4 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/not-match.json @@ -0,0 +1,5 @@ +{ + "name": "should return true if the string does not contain the substring", + "expression": ["notContains", "Hello", "Bye"], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null.json new file mode 100644 index 000000000..206d1f058 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null.json @@ -0,0 +1,5 @@ +{ + "name": "Should always return true if the input is null", + "expression": ["notContains", null, null], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null2.json new file mode 100644 index 000000000..1ff4196b4 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null2.json @@ -0,0 +1,5 @@ +{ + "name": "Should always return true if the input is null", + "expression": ["notContains", "null", null], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null3.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null3.json new file mode 100644 index 000000000..b473ca2a5 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null3.json @@ -0,0 +1,5 @@ +{ + "name": "Should always return true if the input is null", + "expression": ["notContains", null, "null"], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null4.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null4.json new file mode 100644 index 000000000..f0d906f8e --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/notContains/null4.json @@ -0,0 +1,5 @@ +{ + "name": "Should treat stringy nulls as null", + "expression": ["notContains", "null", "null"], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/nearest-integer-with-decimal-as-strings.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/nearest-integer-with-decimal-as-strings.json new file mode 100644 index 000000000..0e7d6530e --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/nearest-integer-with-decimal-as-strings.json @@ -0,0 +1,5 @@ +{ + "name": "Should round a number to the nearest integer with a precision of 2", + "expression": ["round", "2.2199999", "2"], + "expects": "2.22" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/nearest-integer-with-decimal.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/nearest-integer-with-decimal.json new file mode 100644 index 000000000..49642cf5a --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/nearest-integer-with-decimal.json @@ -0,0 +1,5 @@ +{ + "name": "Should round a number to the nearest integer with a precision of 2", + "expression": ["round", 3.2199999, 2], + "expects": "3.22" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/nearest-integer.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/nearest-integer.json new file mode 100644 index 000000000..5856e76c8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/nearest-integer.json @@ -0,0 +1,5 @@ +{ + "name": "Should round to nearest integer", + "expression": ["round", 3.2, null], + "expects": "3" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-0-decimal-places.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-0-decimal-places.json new file mode 100644 index 000000000..1822cf78e --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-0-decimal-places.json @@ -0,0 +1,5 @@ +{ + "name": "Should be able to round 0 to 0 decimal places", + "expression": ["round", 0, 0], + "expects": "0" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-0-decimal-places2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-0-decimal-places2.json new file mode 100644 index 000000000..b79c24939 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-0-decimal-places2.json @@ -0,0 +1,5 @@ +{ + "name": "Should be able to round 0 to 0 decimal places", + "expression": ["round", 2.9999, 0], + "expects": "3" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-negative-number.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-negative-number.json new file mode 100644 index 000000000..63b9e5edd --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-negative-number.json @@ -0,0 +1,5 @@ +{ + "name": "Should round negative number", + "expression": ["round", -2.999, null], + "expects": "-3" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-null.json new file mode 100644 index 000000000..dc1a2c666 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-null.json @@ -0,0 +1,5 @@ +{ + "name": "Should be able to round a null", + "expression": ["round", null], + "expects": "0" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-null2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-null2.json new file mode 100644 index 000000000..bebf3af4a --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-null2.json @@ -0,0 +1,5 @@ +{ + "name": "Should be able to round a null", + "expression": ["round", null, 2], + "expects": "0.00" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-strings.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-strings.json new file mode 100644 index 000000000..b067d7253 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-strings.json @@ -0,0 +1,5 @@ +{ + "name": "Should be able to round a number to the nearest integer even if it is a string", + "expression": ["round", "3.99", null], + "expects": "4" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-with-too-many-args.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-with-too-many-args.json new file mode 100644 index 000000000..603931c73 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-with-too-many-args.json @@ -0,0 +1,5 @@ +{ + "name": "Should fail when given too many arguments", + "expression": ["round", 3.99, 2, 3], + "expectsFailure": "Expected 1-2 argument(s), got 3" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-without-decimalCount.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-without-decimalCount.json new file mode 100644 index 000000000..0bdcbef2b --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-without-decimalCount.json @@ -0,0 +1,5 @@ +{ + "name": "Should be able to round a number without the optional second argument", + "expression": ["round", "3.99"], + "expects": "4" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-without-decimalCount2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-without-decimalCount2.json new file mode 100644 index 000000000..01b5fa64d --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/round/round-without-decimalCount2.json @@ -0,0 +1,5 @@ +{ + "name": "Should be able to round a number without the optional second argument", + "expression": ["round", 3.99], + "expects": "4" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/case-sensitivity.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/case-sensitivity.json new file mode 100644 index 000000000..c9a6c8e3c --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/case-sensitivity.json @@ -0,0 +1,5 @@ +{ + "name": "Should be case sensitive and return false if the string does start with the given string but opposite case", + "expression": ["startsWith", "Hello", "HEL"], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/empty-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/empty-string.json new file mode 100644 index 000000000..5da24b3da --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/empty-string.json @@ -0,0 +1,5 @@ +{ + "name": "All strings start with an empty string", + "expression": ["startsWith", "Hello world", ""], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/exact-match.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/exact-match.json new file mode 100644 index 000000000..70c50773f --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/exact-match.json @@ -0,0 +1,5 @@ +{ + "name": "Should be true when the string starts with the specified string, even on exact match", + "expression": ["startsWith", "Hello", "Hello"], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/not-starts-with.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/not-starts-with.json new file mode 100644 index 000000000..5651cacec --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/not-starts-with.json @@ -0,0 +1,5 @@ +{ + "name": "Should be false when the string does not starts with the specified string", + "expression": ["startsWith", "Hello", "Bye"], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/null.json new file mode 100644 index 000000000..b088b4f16 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/null.json @@ -0,0 +1,5 @@ +{ + "name": "Should always be false when given a null", + "expression": ["startsWith", null, "null"], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/null2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/null2.json new file mode 100644 index 000000000..92656df76 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/null2.json @@ -0,0 +1,5 @@ +{ + "name": "Should always be false when given a null", + "expression": ["startsWith", null, null], + "expects": false +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/number-starts-with-number.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/number-starts-with-number.json new file mode 100644 index 000000000..5d4c14bcd --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/number-starts-with-number.json @@ -0,0 +1,5 @@ +{ + "name": "Should be true when the given number starts with the given digit", + "expression": ["startsWith", 102, 1], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/number-starts-with-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/number-starts-with-string.json new file mode 100644 index 000000000..f14d63a24 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/number-starts-with-string.json @@ -0,0 +1,5 @@ +{ + "name": "Should be true when the number starts with the given string (number starts with string)", + "expression": ["startsWith", 40, "40"], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/start-with-number.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/start-with-number.json new file mode 100644 index 000000000..67b58ef0e --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/start-with-number.json @@ -0,0 +1,5 @@ +{ + "name": "Should be true when the string starts with the given string (even when the given 'string' is a number)", + "expression": ["startsWith", "40 years old", 40], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/start-with.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/start-with.json new file mode 100644 index 000000000..902ef4665 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/startsWith/start-with.json @@ -0,0 +1,5 @@ +{ + "name": "Should be true when the string starts with the specified string", + "expression": ["startsWith", "Hello", "Hel"], + "expects": true +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/empty-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/empty-string.json new file mode 100644 index 000000000..e2249bdca --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/empty-string.json @@ -0,0 +1,5 @@ +{ + "name": "Should return the length of a string even when it's empty", + "expression": ["stringLength", ""], + "expects": 0 +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/length-of-number.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/length-of-number.json new file mode 100644 index 000000000..5bd8f0177 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/length-of-number.json @@ -0,0 +1,5 @@ +{ + "name": "Should return the length of a string even if it is type of number", + "expression": ["stringLength", 203], + "expects": 3 +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/length-of-unicode.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/length-of-unicode.json new file mode 100644 index 000000000..c9b3b9df8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/length-of-unicode.json @@ -0,0 +1,5 @@ +{ + "name": "Should return the number of characters in a unicode string", + "expression": ["stringLength", "æøå"], + "expects": 3 +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/null.json new file mode 100644 index 000000000..c4b7e2ec8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/null.json @@ -0,0 +1,5 @@ +{ + "name": "Should return 0 when the input is a null", + "expression": ["stringLength", null], + "expects": 0 +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/string-length.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/string-length.json new file mode 100644 index 000000000..880a2ae9f --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/string-length.json @@ -0,0 +1,5 @@ +{ + "name": "Should return the length of a string", + "expression": ["stringLength", "Hello"], + "expects": 5 +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/whitespace-length.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/whitespace-length.json new file mode 100644 index 000000000..f77bd7b1e --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/stringLength/whitespace-length.json @@ -0,0 +1,5 @@ +{ + "name": "Should count whitespace as a character when counting string length", + "expression": ["stringLength", " "], + "expects": 1 +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/null.json new file mode 100644 index 000000000..ba4519654 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/null.json @@ -0,0 +1,5 @@ +{ + "name": "Should return a null when the input is a null", + "expression": ["upperCase", null], + "expects": null +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/should-uppercase-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/should-uppercase-string.json new file mode 100644 index 000000000..e283e4372 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/should-uppercase-string.json @@ -0,0 +1,5 @@ +{ + "name": "Should UPPERCASE the string", + "expression": ["upperCase", "hello"], + "expects": "HELLO" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/should-uppercase-whole-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/should-uppercase-whole-string.json new file mode 100644 index 000000000..844d2cbbe --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/should-uppercase-whole-string.json @@ -0,0 +1,5 @@ +{ + "name": "Should UPPERCASE the whole string when given a string has multiple words", + "expression": ["upperCase", "Hola, como estas?"], + "expects": "HOLA, COMO ESTAS?" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/should-uppercase-whole-word.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/should-uppercase-whole-word.json new file mode 100644 index 000000000..45dce5c18 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/should-uppercase-whole-word.json @@ -0,0 +1,5 @@ +{ + "name": "Should UPPERCASE the whole string", + "expression": ["upperCase", "heLlo"], + "expects": "HELLO" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/uppercase-number-should-return-string.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/uppercase-number-should-return-string.json new file mode 100644 index 000000000..60479c63a --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/upperCase/uppercase-number-should-return-string.json @@ -0,0 +1,5 @@ +{ + "name": "Should return number as string when number is passed", + "expression": ["upperCase", 40], + "expects": "40" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-confirm.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-confirm.json new file mode 100644 index 000000000..fb705002e --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-confirm.json @@ -0,0 +1,15 @@ +{ + "name": "Read/Confirm", + "expression": ["and", ["authContext", "read"], ["not", ["authContext", "write"]], ["authContext", "confirm"]], + "expects": true, + "permissions": { + "read": true, + "write": false, + "actions": { + "instantiate": true, + "confirm": true, + "sign": false, + "reject": false + } + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-sign-reject.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-sign-reject.json new file mode 100644 index 000000000..a65aa0a8a --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-sign-reject.json @@ -0,0 +1,15 @@ +{ + "name": "Read/Sign", + "expression": ["and", ["authContext", "read"], ["not", ["authContext", "write"]], ["authContext", "sign"], ["authContext", "reject"]], + "expects": true, + "permissions": { + "read": true, + "write": false, + "actions": { + "instantiate": true, + "confirm": false, + "sign": true, + "reject": true + } + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-sign.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-sign.json new file mode 100644 index 000000000..a674b8a05 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-sign.json @@ -0,0 +1,20 @@ +{ + "name": "Read/Sign", + "expression": [ + "and", + ["authContext", "read"], + ["not", ["authContext", "write"]], + ["authContext", "sign"], + ["not", ["authContext", "reject"]] + ], + "expects": true, + "permissions": { + "read": true, + "write": false, + "actions": { + "instantiate": true, + "confirm": false, + "sign": true + } + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-write.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-write.json new file mode 100644 index 000000000..b34438a8d --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/authContext/read-write.json @@ -0,0 +1,15 @@ +{ + "name": "Read/Write", + "expression": ["and", ["authContext", "read"], ["authContext", "write"]], + "expects": true, + "permissions": { + "read": true, + "write": true, + "actions": { + "instantiate": true, + "confirm": false, + "sign": false, + "reject": false + } + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/language/should-return-nb-if-not-set.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/language/should-return-nb-if-not-set.json new file mode 100644 index 000000000..d5684add6 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/language/should-return-nb-if-not-set.json @@ -0,0 +1,5 @@ +{ + "name": "Should default to nb if no profile is set", + "expression": ["language"], + "expects": "nb" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/language/should-return-profile-settings-preference.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/language/should-return-profile-settings-preference.json new file mode 100644 index 000000000..d97364a87 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/language/should-return-profile-settings-preference.json @@ -0,0 +1,8 @@ +{ + "name": "Should return profileSetting language if set", + "expression": ["language"], + "expects": "en", + "profileSettings": { + "language": "en" + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/language/should-return-selected-language.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/language/should-return-selected-language.json new file mode 100644 index 000000000..281943384 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/language/should-return-selected-language.json @@ -0,0 +1,5 @@ +{ + "name": "Should return 'nb' language as default", + "expression": ["language"], + "expects": "nb" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/null.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/null.json new file mode 100644 index 000000000..fec35efc0 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/null.json @@ -0,0 +1,5 @@ +{ + "name": "Should return null when the input is null", + "expression": ["text", null], + "expects": null +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-key-name-if-key-not-exist.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-key-name-if-key-not-exist.json new file mode 100644 index 000000000..e69a1d3c9 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-key-name-if-key-not-exist.json @@ -0,0 +1,5 @@ +{ + "name": "Should return 'key-name' when key does not exist", + "expression": ["text", "random-key"], + "expects": "random-key" +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource-with-variable-in-rep-group-no-index-markers.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource-with-variable-in-rep-group-no-index-markers.json new file mode 100644 index 000000000..c6cf039f1 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource-with-variable-in-rep-group-no-index-markers.json @@ -0,0 +1,114 @@ +{ + "name": "Should text resource with resolved variable inside a repeating group, without index markers", + "disabledFrontend": true, + "expression": ["text", "found.key"], + "expects": "Hello world Arne", + "context": { + "component": "myndig", + "rowIndices": [1, 0], + "currentLayout": "Page2" + }, + "textResources": [ + { + "id": "found.key", + "value": "Hello world {0}", + "variables": [ + { + "key": "Bedrifter.Ansatte.Navn", + "dataSource": "dataModel.default" + } + ] + } + ], + "layouts": { + "Page1": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [] + } + }, + "Page2": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "bedrifter", + "type": "Group", + "maxCount": 99, + "dataModelBindings": { + "group": "Bedrifter" + }, + "children": ["bedriftsNavn", "ansatte"] + }, + { + "id": "bedriftsNavn", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "Bedrifter.Navn" + } + }, + { + "id": "ansatte", + "type": "Group", + "maxCount": 99, + "dataModelBindings": { + "group": "Bedrifter.Ansatte" + }, + "children": ["navn", "alder", "myndig"] + }, + { + "id": "navn", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "Bedrifter.Ansatte.Navn" + } + }, + { + "id": "alder", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "Bedrifter.Ansatte.Alder" + } + }, + { + "id": "myndig", + "type": "Paragraph", + "textResourceBindings": { + "title": "Hurra, den ansatte er myndig!" + } + } + ] + } + } + }, + "dataModel": { + "Bedrifter": [ + { + "Navn": "Hell og lykke AS", + "Ansatte": [ + { + "Navn": "Kaare", + "Alder": 24 + }, + { + "Navn": "Per", + "Alder": 24 + } + ] + }, + { + "Navn": "Nedtur og motgang AS", + "Ansatte": [ + { + "Navn": "Arne", + "Alder": 24 + }, + { + "Navn": "Vidar", + "Alder": 14 + } + ] + } + ] + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource-with-variable-in-rep-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource-with-variable-in-rep-group.json new file mode 100644 index 000000000..c6e2f1756 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource-with-variable-in-rep-group.json @@ -0,0 +1,114 @@ +{ + "name": "Should text resource with resolved variable inside a repeating group", + "disabledFrontend": true, + "expression": ["text", "found.key"], + "expects": "Hello world Vidar", + "context": { + "component": "myndig", + "rowIndices": [1, 1], + "currentLayout": "Page2" + }, + "textResources": [ + { + "id": "found.key", + "value": "Hello world {0}", + "variables": [ + { + "key": "Bedrifter[{0}].Ansatte[{1}].Navn", + "dataSource": "dataModel.default" + } + ] + } + ], + "layouts": { + "Page1": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [] + } + }, + "Page2": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "bedrifter", + "type": "Group", + "maxCount": 99, + "dataModelBindings": { + "group": "Bedrifter" + }, + "children": ["bedriftsNavn", "ansatte"] + }, + { + "id": "bedriftsNavn", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "Bedrifter.Navn" + } + }, + { + "id": "ansatte", + "type": "Group", + "maxCount": 99, + "dataModelBindings": { + "group": "Bedrifter.Ansatte" + }, + "children": ["navn", "alder", "myndig"] + }, + { + "id": "navn", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "Bedrifter.Ansatte.Navn" + } + }, + { + "id": "alder", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "Bedrifter.Ansatte.Alder" + } + }, + { + "id": "myndig", + "type": "Paragraph", + "textResourceBindings": { + "title": "Hurra, den ansatte er myndig!" + } + } + ] + } + } + }, + "dataModel": { + "Bedrifter": [ + { + "Navn": "Hell og lykke AS", + "Ansatte": [ + { + "Navn": "Kaare", + "Alder": 24 + }, + { + "Navn": "Per", + "Alder": 24 + } + ] + }, + { + "Navn": "Nedtur og motgang AS", + "Ansatte": [ + { + "Navn": "Arne", + "Alder": 24 + }, + { + "Navn": "Vidar", + "Alder": 14 + } + ] + } + ] + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource-with-variable.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource-with-variable.json new file mode 100644 index 000000000..19b5dbaa7 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource-with-variable.json @@ -0,0 +1,25 @@ +{ + "name": "Should text resource with resolved variable", + "disabledFrontend": true, + "expression": ["text", "found.key"], + "expects": "Hello world foo bar", + "textResources": [ + { + "id": "found.key", + "value": "Hello world {0}", + "variables": [ + { + "key": "My.Model.Value", + "dataSource": "dataModel.default" + } + ] + } + ], + "dataModel": { + "My": { + "Model": { + "Value": "foo bar" + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource.json new file mode 100644 index 000000000..06fa4cce9 --- /dev/null +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/up-for-evaluation/functions/text/should-return-text-resource.json @@ -0,0 +1,11 @@ +{ + "name": "Should return text resource value", + "expression": ["text", "found.key"], + "expects": "Hello world", + "textResources": [ + { + "id": "found.key", + "value": "Hello world" + } + ] +} diff --git a/test/Altinn.App.Core.Tests/Options/AppOptionsFactoryTests.cs b/test/Altinn.App.Core.Tests/Options/AppOptionsFactoryTests.cs index 527d9d4d3..c83607b48 100644 --- a/test/Altinn.App.Core.Tests/Options/AppOptionsFactoryTests.cs +++ b/test/Altinn.App.Core.Tests/Options/AppOptionsFactoryTests.cs @@ -71,6 +71,18 @@ public void GetOptionsProvider_CustomOptionsProviderWithUpperCase_ShouldReturnCu optionsProvider.Id.Should().Be("country"); } + [Fact] + public async Task GetParameters_CustomOptionsProviderWithUpperCase_ShouldReturnCustomType() + { + var appOptionsFileHandler = new Mock(); + var factory = new AppOptionsFactory(new List() { new DefaultAppOptionsProvider(appOptionsFileHandler.Object), new CountryAppOptionsProvider() }); + + IAppOptionsProvider optionsProvider = factory.GetOptionsProvider("Country"); + + AppOptions options = await optionsProvider.GetAppOptionsAsync("nb", new Dictionary() { { "key", "value" } }); + options.Parameters.First(x => x.Key == "key").Value.Should().Be("value"); + } + internal class CountryAppOptionsProvider : IAppOptionsProvider { public string Id { get; set; } = "country"; @@ -91,7 +103,9 @@ public Task GetAppOptionsAsync(string language, Dictionary Date: Tue, 8 Aug 2023 15:07:13 +0200 Subject: [PATCH 13/46] merge fix --- .../Extensions/ServiceCollectionExtensions.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index eed33e951..80cf71a9b 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -264,17 +264,5 @@ private static void AddFileValidatorServices(IServiceCollection services) services.TryAddTransient(); services.TryAddTransient(); } - - private static void AddFileAnalyserServices(IServiceCollection services) - { - services.TryAddTransient(); - services.TryAddTransient(); - } - - private static void AddFileValidatorServices(IServiceCollection services) - { - services.TryAddTransient(); - services.TryAddTransient(); - } } } From 86a73770ac1e669597b8421ec493a82bdc47883a Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Tue, 8 Aug 2023 15:41:57 +0200 Subject: [PATCH 14/46] Remove some codesmells --- .../Controllers/ProcessController.cs | 32 +++++++++---------- src/Altinn.App.Api/Models/ProcessNext.cs | 1 + .../Features/Action/SigningUserAction.cs | 10 +++--- src/Altinn.App.Core/Features/IUserAction.cs | 11 +++++++ .../Clients/Register/AltinnPartyClient.cs | 3 -- .../Process/ExpressionsExclusiveGateway.cs | 14 ++++---- .../Internal/Process/ProcessEngine.cs | 4 +-- .../Models/UserAction/UserActionContext.cs | 14 ++++++++ .../Features/Action/SigningUserActionTests.cs | 9 +----- 9 files changed, 56 insertions(+), 42 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index 9f9eb47c6..f3e29404e 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -1,9 +1,9 @@ +#nullable enable using System.Net; using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Models; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; @@ -115,9 +115,9 @@ public async Task> StartProcess( [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, [FromRoute] Guid instanceGuid, - [FromQuery] string startEvent = null) + [FromQuery] string? startEvent = null) { - Instance instance = null; + Instance? instance = null; try { @@ -170,8 +170,8 @@ public async Task>> GetNextElements( [FromRoute] int instanceOwnerPartyId, [FromRoute] Guid instanceGuid) { - Instance instance = null; - string currentTaskId = null; + Instance? instance = null; + string? currentTaskId = null; try { @@ -244,8 +244,8 @@ public async Task> NextElement( [FromRoute] string app, [FromRoute] int instanceOwnerPartyId, [FromRoute] Guid instanceGuid, - [FromQuery] string elementId = null, - [FromQuery] string lang = null) + [FromQuery] string? elementId = null, + [FromQuery] string? lang = null) { try { @@ -267,7 +267,7 @@ public async Task> NextElement( return Conflict($"Process is ended."); } - string altinnTaskType = instance.Process.CurrentTask?.AltinnTaskType; + string? altinnTaskType = instance?.Process?.CurrentTask?.AltinnTaskType; if (altinnTaskType == null) { @@ -275,8 +275,8 @@ public async Task> NextElement( } bool authorized; - string checkedAction = EnsureActionNotTaskType(processNext?.Action ?? altinnTaskType); - authorized = await AuthorizeAction(checkedAction, org, app, instanceOwnerPartyId, instanceGuid, instance.Process.CurrentTask?.ElementId); + string? checkedAction = EnsureActionNotTaskType(processNext?.Action ?? altinnTaskType); + authorized = await AuthorizeAction(checkedAction, org, app, instanceOwnerPartyId, instanceGuid, instance?.Process?.CurrentTask?.ElementId); if (!authorized) { @@ -370,9 +370,9 @@ public async Task> CompleteProcess( int counter = 0; do { - string altinnTaskType = EnsureActionNotTaskType(instance.Process.CurrentTask?.AltinnTaskType); + string? altinnTaskType = EnsureActionNotTaskType(instance?.Process?.CurrentTask?.AltinnTaskType); - bool authorized = await AuthorizeAction(altinnTaskType, org, app, instanceOwnerPartyId, instanceGuid, instance.Process.CurrentTask?.ElementId); + bool authorized = await AuthorizeAction(altinnTaskType, org, app, instanceOwnerPartyId, instanceGuid, instance?.Process?.CurrentTask?.ElementId); if (!authorized) { return Forbid(); @@ -413,7 +413,7 @@ public async Task> CompleteProcess( counter++; } - while (instance.Process.EndEvent == null || counter > MaxIterationsAllowed); + while (instance?.Process?.EndEvent == null || counter > MaxIterationsAllowed); if (counter > MaxIterationsAllowed) { @@ -491,12 +491,12 @@ private ActionResult ExceptionResponse(Exception exception, string message) return StatusCode(500, $"{message}"); } - private async Task AuthorizeAction(string action, string org, string app, int instanceOwnerPartyId, Guid instanceGuid, string taskId = null) + private async Task AuthorizeAction(string action, string org, string app, int instanceOwnerPartyId, Guid instanceGuid, string? taskId = null) { return await _authorization.AuthorizeAction(new AppIdentifier(org, app), new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), HttpContext.User, action, taskId); } - private static string EnsureActionNotTaskType(string actionOrTaskType) + private static string? EnsureActionNotTaskType(string? actionOrTaskType) { switch (actionOrTaskType) { @@ -531,7 +531,7 @@ private ActionResult HandlePlatformHttpException(PlatformHttpException e, string return ExceptionResponse(e, defaultMessage); } - private static async Task DeserializeFromStream(Stream stream) + private static async Task DeserializeFromStream(Stream stream) { using StreamReader reader = new StreamReader(stream); string text = await reader.ReadToEndAsync(); diff --git a/src/Altinn.App.Api/Models/ProcessNext.cs b/src/Altinn.App.Api/Models/ProcessNext.cs index 675e9f841..cfa277a87 100644 --- a/src/Altinn.App.Api/Models/ProcessNext.cs +++ b/src/Altinn.App.Api/Models/ProcessNext.cs @@ -1,3 +1,4 @@ +#nullable enable using System.Text.Json.Serialization; namespace Altinn.App.Api.Models; diff --git a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs index db065791e..f450bdf7f 100644 --- a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs +++ b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs @@ -1,5 +1,4 @@ using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Profile; @@ -19,7 +18,6 @@ public class SigningUserAction: IUserAction { private readonly IProcessReader _processReader; private readonly ILogger _logger; - private readonly IAppMetadata _appMetadata; private readonly IProfileClient _profileClient; private readonly ISignClient _signClient; @@ -28,11 +26,11 @@ public class SigningUserAction: IUserAction /// /// The process reader /// The logger - /// The application metadata service - public SigningUserAction(IProcessReader processReader, ILogger logger, IAppMetadata appMetadata, IProfileClient profileClient, ISignClient signClient) + /// The profile client + /// The sign client + public SigningUserAction(IProcessReader processReader, ILogger logger, IProfileClient profileClient, ISignClient signClient) { _logger = logger; - _appMetadata = appMetadata; _profileClient = profileClient; _signClient = signClient; _processReader = processReader; @@ -64,7 +62,7 @@ public async Task HandleAction(UserActionContext context) return false; } - private List GetDataElementSignatures(List dataElements, List dataTypesToSign) + private static List GetDataElementSignatures(List dataElements, List dataTypesToSign) { var connectedDataElements = new List(); foreach (var dataType in dataTypesToSign) diff --git a/src/Altinn.App.Core/Features/IUserAction.cs b/src/Altinn.App.Core/Features/IUserAction.cs index 2f1b5d652..61052b564 100644 --- a/src/Altinn.App.Core/Features/IUserAction.cs +++ b/src/Altinn.App.Core/Features/IUserAction.cs @@ -2,9 +2,20 @@ namespace Altinn.App.Core.Features; +/// +/// Interface for implementing custom code for user actions +/// public interface IUserAction { + /// + /// The id of the user action + /// string Id { get; } + /// + /// Method for handling the user action + /// + /// The user action context + /// If the handling of the action was a success Task HandleAction(UserActionContext context); } \ No newline at end of file diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs index f5a03058d..b7c90cbfa 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs @@ -33,8 +33,6 @@ public class AltinnPartyClient : IAltinnPartyClient /// Initializes a new instance of the class /// /// The current platform settings. - /// The dsf - /// The organizationClient /// The logger /// The http context accessor /// The application settings. @@ -43,7 +41,6 @@ public class AltinnPartyClient : IAltinnPartyClient /// The platform access token generator public AltinnPartyClient( IOptions platformSettings, - IOrganizationClient organizationClient, ILogger logger, IHttpContextAccessor httpContextAccessor, IOptionsMonitor settings, diff --git a/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs b/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs index 26c441a45..f105e2210 100644 --- a/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs +++ b/src/Altinn.App.Core/Internal/Process/ExpressionsExclusiveGateway.cs @@ -77,7 +77,7 @@ private async Task GetLayoutEvaluatorState(Instance instan return state; } - private bool EvaluateSequenceFlow(LayoutEvaluatorState state, SequenceFlow sequenceFlow) + private static bool EvaluateSequenceFlow(LayoutEvaluatorState state, SequenceFlow sequenceFlow) { if (sequenceFlow.ConditionExpression != null) { @@ -123,8 +123,8 @@ private static Expression GetExpressionFromCondition(string condition) LayoutSet? layoutSet = null; if (!string.IsNullOrEmpty(layoutSetsString)) { - LayoutSets? layoutSets = JsonSerializer.Deserialize(layoutSetsString, options)!; - layoutSet = layoutSets?.Sets?.FirstOrDefault(t => t.Tasks.Contains(taskId)); + LayoutSets? layoutSets = JsonSerializer.Deserialize(layoutSetsString, options); + layoutSet = layoutSets?.Sets?.Find(t => t.Tasks.Contains(taskId)); } return layoutSet; @@ -136,15 +136,15 @@ private static Expression GetExpressionFromCondition(string condition) DataType? dataType; if (dataTypeId != null) { - dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.FirstOrDefault(d => d.Id == dataTypeId && d.AppLogic != null); + dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.Find(d => d.Id == dataTypeId && d.AppLogic != null); } else if (layoutSet != null) { - dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.FirstOrDefault(d => d.Id == layoutSet.DataType && d.AppLogic != null); + dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.Find(d => d.Id == layoutSet.DataType && d.AppLogic != null); } else { - dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.FirstOrDefault(d => d.TaskId == instance.Process.CurrentTask.ElementId && d.AppLogic != null); + dataType = (await _appMetadata.GetApplicationMetadata()).DataTypes.Find(d => d.TaskId == instance.Process.CurrentTask.ElementId && d.AppLogic != null); } if (dataType != null) @@ -157,7 +157,7 @@ private static Expression GetExpressionFromCondition(string condition) private static Guid? GetDataId(Instance instance, string dataType) { - string? dataId = instance.Data.FirstOrDefault(d => d.DataType == dataType)?.Id; + string? dataId = instance.Data.Find(d => d.DataType == dataType)?.Id; if (dataId != null) { return new Guid(dataId); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 7a17004ef..08f1ece2f 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -72,9 +72,9 @@ public async Task StartProcess(ProcessStartRequest processS // start process ProcessStateChange? startChange = await ProcessStart(processStartRequest.Instance, validStartElement!, processStartRequest.User); - InstanceEvent? startEvent = startChange?.Events?.First().CopyValues(); + InstanceEvent? startEvent = startChange?.Events?[0].CopyValues(); ProcessStateChange? nextChange = await ProcessNext(processStartRequest.Instance, processStartRequest.User); - InstanceEvent? goToNextEvent = nextChange?.Events?.First().CopyValues(); + InstanceEvent? goToNextEvent = nextChange?.Events?[0].CopyValues(); List events = new List(); if (startEvent is not null) { diff --git a/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs b/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs index 1f8ea3136..1076c49e2 100644 --- a/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs +++ b/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs @@ -2,15 +2,29 @@ namespace Altinn.App.Core.Models.UserAction; +/// +/// Context for user actions +/// public class UserActionContext { + /// + /// Creates a new instance of the class + /// + /// The instance the action is performed on + /// The user performing the action public UserActionContext(Instance instance, int userId) { Instance = instance; UserId = userId; } + /// + /// The instance the action is performed on + /// public Instance Instance { get; } + /// + /// The user performing the action + /// public int UserId { get; } } \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs index 552e47845..7b77a6c26 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs @@ -111,14 +111,7 @@ public async void HandleAction_throws_ApplicationConfigException_if_SignatureDat private static (SigningUserAction SigningUserAction, Mock SignClientMock) CreateSigningUserAction(UserProfile userProfileToReturn = null, PlatformHttpException platformHttpExceptionToThrow = null, string testBpmnfilename = "signing-task-process.bpmn") { IProcessReader processReader = ProcessTestUtils.SetupProcessReader(testBpmnfilename, Path.Combine("Features", "Action", "TestData")); - AppSettings appSettings = new AppSettings() - { - AppBasePath = Path.Combine("Features", "Action"), - ConfigurationFolder = "TestData", - ApplicationMetadataFileName = "appmetadata.json" - }; - IAppMetadata appMetadata = new AppMetadata(Options.Create(appSettings), new FrontendFeatures(new Mock().Object)); var profileClientMock = new Mock(); var signingClientMock = new Mock(); profileClientMock.Setup(p => p.GetUserProfile(It.IsAny())).ReturnsAsync(userProfileToReturn); @@ -127,7 +120,7 @@ private static (SigningUserAction SigningUserAction, Mock SignClien signingClientMock.Setup(p => p.SignDataElements(It.IsAny())).ThrowsAsync(platformHttpExceptionToThrow); } - return (new SigningUserAction(processReader, new NullLogger(), appMetadata, profileClientMock.Object, signingClientMock.Object), signingClientMock); + return (new SigningUserAction(processReader, new NullLogger(), profileClientMock.Object, signingClientMock.Object), signingClientMock); } private bool AssertSigningContextAsExpected(SignatureContext s1, SignatureContext s2) From 6ce0799b206fa8bbb14c77e6f4fd34b78082dc4f Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Thu, 17 Aug 2023 09:46:35 +0200 Subject: [PATCH 15/46] Separate metrics port (#288) * Make Metrics configurable through appsettings.json and move endpoint to separate port * add missing trailing linebreaks --- .../Extensions/ServiceCollectionExtensions.cs | 18 +++++++++-- .../WebApplicationBuilderExtensions.cs | 30 ++++++++++++++----- .../Configuration/MetricsSettings.cs | 16 ++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 14 +++++++-- test/Altinn.App.Api.Tests/Program.cs | 18 ++--------- 5 files changed, 68 insertions(+), 28 deletions(-) create mode 100644 src/Altinn.App.Core/Configuration/MetricsSettings.cs diff --git a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs index 0c7512386..a7417917c 100644 --- a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable +using System; +using Altinn.App.Api.Configuration; using Altinn.App.Api.Controllers; using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Infrastructure.Health; @@ -18,6 +20,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.FeatureManagement; using Microsoft.IdentityModel.Tokens; +using Prometheus; namespace Altinn.App.Api.Extensions { @@ -74,6 +77,7 @@ public static void AddAltinnAppServices(this IServiceCollection services, IConfi services.AddHttpClient(); services.AddSingleton(); + services.AddMetricsServer(config); } private static void AddApplicationInsights(IServiceCollection services, IConfiguration config, IWebHostEnvironment env) @@ -144,5 +148,15 @@ private static void AddAntiforgery(IServiceCollection services) services.TryAddSingleton(); } + + private static void AddMetricsServer(this IServiceCollection services, IConfiguration config) + { + var metricsSettings = config.GetSection("MetricsSettings").Get() ?? new MetricsSettings(); + if (metricsSettings.Enabled) + { + ushort port = metricsSettings.Port; + services.AddMetricServer(options => { options.Port = port; }); + } + } } -} \ No newline at end of file +} diff --git a/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs b/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs index 400dd87a8..d6fb4613d 100644 --- a/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs +++ b/src/Altinn.App.Api/Extensions/WebApplicationBuilderExtensions.cs @@ -1,4 +1,6 @@ +#nullable enable using System.Reflection; +using Altinn.App.Api.Configuration; using Altinn.App.Api.Helpers; using Prometheus; @@ -16,20 +18,15 @@ public static class WebApplicationBuilderExtensions /// public static IApplicationBuilder UseAltinnAppCommonConfiguration(this IApplicationBuilder app) { + var appId = StartupHelper.GetApplicationId(); if (app is WebApplication webApp && webApp.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); + webApp.UseAltinnPrometheus(appId); } app.UseHttpMetrics(); app.UseMetricServer(); - var appId = StartupHelper.GetApplicationId(); - var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString(); - Metrics.DefaultRegistry.SetStaticLabels(new Dictionary() - { - { "application_id", appId }, - { "nuget_package_version", version } - }); app.UseDefaultSecurityHeaders(); app.UseRouting(); app.UseStaticFiles('/' + appId); @@ -43,4 +40,21 @@ public static IApplicationBuilder UseAltinnAppCommonConfiguration(this IApplicat app.UseHealthChecks("/health"); return app; } -} \ No newline at end of file + + private static void UseAltinnPrometheus(this WebApplication webApp, string appId) + { + var metricsSettings = webApp.Configuration.GetSection("MetricsSettings")?.Get() ?? new MetricsSettings(); + if (!metricsSettings.Enabled) + { + return; + } + + webApp.UseHttpMetrics(); + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; + Metrics.DefaultRegistry.SetStaticLabels(new Dictionary() + { + { "application_id", appId }, + { "nuget_package_version", version } + }); + } +} diff --git a/src/Altinn.App.Core/Configuration/MetricsSettings.cs b/src/Altinn.App.Core/Configuration/MetricsSettings.cs new file mode 100644 index 000000000..3de05e3df --- /dev/null +++ b/src/Altinn.App.Core/Configuration/MetricsSettings.cs @@ -0,0 +1,16 @@ +namespace Altinn.App.Api.Configuration; + +/// +/// Metric settings for Altinn Apps +/// +public class MetricsSettings +{ + /// + /// Gets or sets a value indicating whether metrics is enabled or not + /// + public bool Enabled { get; set; } = true; + /// + /// Gets or sets the port for the metrics server is exposed + /// + public ushort Port { get; set; } = 5006; +} diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 80cf71a9b..aad701e8e 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,3 +1,4 @@ +using Altinn.App.Api.Configuration; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; @@ -80,7 +81,6 @@ public static void AddPlatformServices(this IServiceCollection services, IConfig services.AddHttpClient(); services.AddHttpClient(); services.AddHttpClient(); - services.Decorate(); services.AddHttpClient(); services.AddHttpClient(); services.AddHttpClient(); @@ -162,6 +162,7 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati AddProcessServices(services); AddFileAnalyserServices(services); AddFileValidatorServices(services); + AddMetricsDecorators(services, configuration); if (!env.IsDevelopment()) { @@ -236,7 +237,6 @@ private static void AddAppOptions(IServiceCollection services) private static void AddProcessServices(IServiceCollection services) { services.TryAddTransient(); - services.Decorate(); services.TryAddTransient(); services.TryAddSingleton(); services.TryAddTransient(); @@ -264,5 +264,15 @@ private static void AddFileValidatorServices(IServiceCollection services) services.TryAddTransient(); services.TryAddTransient(); } + + private static void AddMetricsDecorators(IServiceCollection services, IConfiguration configuration) + { + MetricsSettings metricsSettings = configuration.GetSection("MetricsSettings")?.Get() ?? new MetricsSettings(); + if (metricsSettings.Enabled) + { + services.Decorate(); + services.Decorate(); + } + } } } diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs index 1b6fa9e3f..520fc5a40 100644 --- a/test/Altinn.App.Api.Tests/Program.cs +++ b/test/Altinn.App.Api.Tests/Program.cs @@ -24,6 +24,7 @@ // external api's etc. should be mocked. WebApplicationBuilder builder = WebApplication.CreateBuilder(new WebApplicationOptions() { ApplicationName = "Altinn.App.Api.Tests" }); +builder.Configuration.GetSection("MetricsSettings:Enabled").Value = "false"; ConfigureServices(builder.Services, builder.Configuration); ConfigureMockServices(builder.Services, builder.Configuration); @@ -56,22 +57,7 @@ void ConfigureMockServices(IServiceCollection services, ConfigurationManager con void Configure() { - if (app.Environment.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseDefaultSecurityHeaders(); - app.UseRouting(); - app.UseStaticFiles(); - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - app.UseHealthChecks("/health"); + app.UseAltinnAppCommonConfiguration(); } // This "hack" (documentet by Microsoft) is done to From d21de61cc64d57b4a26ad94a0f52ff82e75a1e8a Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Mon, 28 Aug 2023 12:52:05 +0200 Subject: [PATCH 16/46] Add tool for upgrading from v7 to v8 (#291) * Add tool for upgrading from v7 to v8 * rename cli tool and include release pipeline --- .github/workflows/publish-release.yml | 41 +++- cli-tools/altinn-app-cli/Makefile | 42 ++++ cli-tools/altinn-app-cli/Program.cs | 191 ++++++++++++++++++ .../altinn-app-cli/altinn-app-cli.csproj | 42 ++++ .../v7Tov8/CodeRewriters/TypesRewriter.cs | 152 ++++++++++++++ .../v7Tov8/CodeRewriters/UsingRewriter.cs | 91 +++++++++ .../v7Tov8/ProcessRewriter/ProcessUpgrader.cs | 168 +++++++++++++++ .../v7Tov8/ProjectChecks/ProjectChecks.cs | 71 +++++++ .../ProjectRewriters/ProjectFileRewriter.cs | 53 +++++ 9 files changed, 847 insertions(+), 4 deletions(-) create mode 100644 cli-tools/altinn-app-cli/Makefile create mode 100644 cli-tools/altinn-app-cli/Program.cs create mode 100644 cli-tools/altinn-app-cli/altinn-app-cli.csproj create mode 100644 cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/TypesRewriter.cs create mode 100644 cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/UsingRewriter.cs create mode 100644 cli-tools/altinn-app-cli/v7Tov8/ProcessRewriter/ProcessUpgrader.cs create mode 100644 cli-tools/altinn-app-cli/v7Tov8/ProjectChecks/ProjectChecks.cs create mode 100644 cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 31783f348..0686bb30c 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,4 +1,4 @@ -name: Pack and publish nugets +name: Pack and publish on: release: @@ -6,7 +6,8 @@ on: - published jobs: - build-pack: + release-nugets: + if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -17,7 +18,6 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 5.0.x 6.0.x - name: Install deps run: | @@ -33,4 +33,37 @@ jobs: dotnet --version - name: Publish run: | - dotnet nuget push src/**/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} \ No newline at end of file + dotnet nuget push src/**/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} + release-upgrade-tool: + if: startsWith(github.ref, 'refs/tags/altinn-app-cli') + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./cli-tools/altinn-app-cli + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Install dotnet6 + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.0.x + - name: Build bundles + run: | + make bundles + - name: Upload files to release + uses: softprops/action-gh-release@v1 + with: + files: | + publish/archives/osx-x64.tar.gz + publish/archives/osx-arm64.tar.gz + publish/archives/linux-x64.tar.gz + publish/archives/linux-arm64.tar.gz + publish/archives/win-x64.zip + publish/archives/osx-x64.tar.gz.sha512 + publish/archives/osx-arm64.tar.gz.sha512 + publish/archives/linux-x64.tar.gz.sha512 + publish/archives/linux-arm64.tar.gz.sha512 + publish/archives/win-x64.zip.sha512 + \ No newline at end of file diff --git a/cli-tools/altinn-app-cli/Makefile b/cli-tools/altinn-app-cli/Makefile new file mode 100644 index 000000000..6cae575a1 --- /dev/null +++ b/cli-tools/altinn-app-cli/Makefile @@ -0,0 +1,42 @@ +# Path: Makefile + +build: + dotnet build + +executable-osx-x64: + dotnet publish -c Release -o publish/osx-x64 -r osx-x64 --self-contained + +executable-osx-arm64: + dotnet publish -c Release -o publish/osx-arm64 -r osx-arm64 --self-contained + +executable-win-x64: + dotnet publish -c Release -o publish/win-x64 -r win-x64 --self-contained + +executable-linux-x64: + dotnet publish -c Release -o publish/linux-x64 -r linux-x64 --self-contained + +executable-linux-arm64: + dotnet publish -c Release -o publish/linux-arm64 -r linux-arm64 --self-contained + +executables: executable-osx-x64 executable-osx-arm64 executable-win-x64 executable-linux-x64 executable-linux-arm64 + +archives: + mkdir -p publish/archives + tar -czvf publish/archives/osx-x64.tar.gz publish/osx-x64/altinn-app-cli + tar -czvf publish/archives/osx-arm64.tar.gz publish/osx-arm64/altinn-app-cli + tar -czvf publish/archives/linux-x64.tar.gz publish/linux-x64/altinn-app-cli + tar -czvf publish/archives/linux-arm64.tar.gz publish/linux-arm64/altinn-app-cli + zip -r publish/archives/win-x64.zip publish/win-x64/altinn-app-cli.exe + +checksums: + sha512sum publish/archives/osx-x64.tar.gz > publish/archives/osx-x64.tar.gz.sha512 + sha512sum publish/archives/osx-arm64.tar.gz > publish/archives/osx-arm64.tar.gz.sha512 + sha512sum publish/archives/linux-x64.tar.gz > publish/archives/linux-x64.tar.gz.sha512 + sha512sum publish/archives/linux-arm64.tar.gz > publish/archives/linux-arm64.tar.gz.sha512 + sha512sum publish/archives/win-x64.zip > publish/archives/win-x64.zip.sha512 + +bundles: executables archives checksums + +clean: + dotnet clean + rm -rf publish/ \ No newline at end of file diff --git a/cli-tools/altinn-app-cli/Program.cs b/cli-tools/altinn-app-cli/Program.cs new file mode 100644 index 000000000..17a257d7a --- /dev/null +++ b/cli-tools/altinn-app-cli/Program.cs @@ -0,0 +1,191 @@ +using System.CommandLine; +using System.Reflection; +using altinn_app_cli.v7Tov8.CodeRewriters; +using altinn_app_cli.v7Tov8.ProcessRewriter; +using altinn_app_cli.v7Tov8.ProjectChecks; +using altinn_app_cli.v7Tov8.ProjectRewriters; +using Microsoft.Build.Locator; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.MSBuild; + +namespace altinn_app_upgrade_cli; + +class Program +{ + static async Task Main(string[] args) + { + int returnCode = 0; + var projectFolderOption = new Option(name: "--folder", description: "The project folder to read", getDefaultValue: () => "CurrentDirectory"); + var projectFileOption = new Option(name: "--project", description: "The project file to read relative to --folder", getDefaultValue: () => "App/App.csproj"); + var processFileOption = new Option(name: "--process", description: "The process file to read relative to --folder", getDefaultValue: () => "App/config/process/process.bpmn"); + var targetVersionOption = new Option(name: "--target-version", description: "The target version to upgrade to", getDefaultValue: () => "8.0.0-preview.9"); + var skipCsprojUpgradeOption = new Option(name: "--skip-csproj-upgrade", description: "Skip csproj file upgrade", getDefaultValue: () => false); + var skipCodeUpgradeOption = new Option(name: "--skip-code-upgrade", description: "Skip code upgrade", getDefaultValue: () => false); + var skipProcessUpgradeOption = new Option(name: "--skip-process-upgrade", description: "Skip process file upgrade", getDefaultValue: () => false); + var rootCommand = new RootCommand("Command line interface for working with Altinn 3 Applications"); + var upgradeCommand = new Command("upgrade", "Upgrade an app from v7 to v8") + { + projectFolderOption, + projectFileOption, + processFileOption, + targetVersionOption, + skipCsprojUpgradeOption, + skipCodeUpgradeOption, + skipProcessUpgradeOption, + }; + rootCommand.AddCommand(upgradeCommand); + var versionCommand = new Command("version", "Print version of altinn-app-cli"); + rootCommand.AddCommand(versionCommand); + + upgradeCommand.SetHandler(async (projectFolder, projectFile, processFile, targetVersion, skipCodeUpgrade, skipProcessUpgrade, skipCsprojUpgrade) => + { + if (projectFolder == "CurrentDirectory") + { + projectFolder = Directory.GetCurrentDirectory(); + } + + if (File.Exists(projectFolder)) + { + Console.WriteLine($"Project folder {projectFolder} does not exist. Please supply location of project with --folder [path/to/project]"); + returnCode = 1; + return; + } + + FileAttributes attr = File.GetAttributes(projectFolder); + if ((attr & FileAttributes.Directory) != FileAttributes.Directory) + { + Console.WriteLine($"Project folder {projectFolder} is a file. Please supply location of project with --folder [path/to/project]"); + returnCode = 1; + return; + } + + if (!Path.IsPathRooted(projectFolder)) + { + projectFile = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, projectFile); + processFile = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, processFile); + } + else + { + projectFile = Path.Combine(projectFolder, projectFile); + processFile = Path.Combine(projectFolder, processFile); + } + + var projectChecks = new ProjectChecks(projectFile); + if (!projectChecks.SupportedSourceVersion()) + { + Console.WriteLine($"Version(s) in project file {projectFile} is not supported. Please upgrade to version 7.0.0 or higher."); + returnCode = 2; + return; + } + if (!skipCsprojUpgrade) + { + returnCode = await UpgradeNugetVersions(projectFile, targetVersion); + } + + if (!skipCodeUpgrade && returnCode == 0) + { + returnCode = await UpgradeCode(projectFile); + } + + if (!skipProcessUpgrade && returnCode == 0) + { + returnCode = await UpgradeProcess(processFile); + } + + if (returnCode == 0) + { + Console.WriteLine("Upgrade completed without errors. Please verify that the application is still working as expected."); + } + else + { + Console.WriteLine("Upgrade completed with errors. Please check for errors in the log above."); + } + }, + projectFolderOption, projectFileOption, processFileOption, targetVersionOption, skipCodeUpgradeOption, skipProcessUpgradeOption, skipCsprojUpgradeOption); + versionCommand.SetHandler(() => + { + var version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; + Console.WriteLine($"altinn-app-cli v{version}"); + }); + await rootCommand.InvokeAsync(args); + return returnCode; + } + + static async Task UpgradeNugetVersions(string projectFile, string targetVersion) + { + if (!File.Exists(projectFile)) + { + Console.WriteLine($"Project file {projectFile} does not exist. Please supply location of project with --project [path/to/project.csproj]"); + return 1; + } + + Console.WriteLine("Trying to upgrade nuget versions in project file"); + var rewriter = new ProjectFileRewriter(projectFile, targetVersion); + await rewriter.Upgrade(); + Console.WriteLine("Nuget versions upgraded"); + return 0; + } + + static async Task UpgradeCode(string projectFile) + { + if (!File.Exists(projectFile)) + { + Console.WriteLine($"Project file {projectFile} does not exist. Please supply location of project with --project [path/to/project.csproj]"); + return 1; + } + + Console.WriteLine("Trying to upgrade references and using in code"); + + MSBuildLocator.RegisterDefaults(); + var workspace = MSBuildWorkspace.Create(); + var project = await workspace.OpenProjectAsync(projectFile); + var comp = await project.GetCompilationAsync(); + if (comp == null) + { + Console.WriteLine("Could not get compilation"); + return 1; + } + foreach (var sourceTree in comp.SyntaxTrees) + { + SemanticModel sm = comp.GetSemanticModel(sourceTree); + TypesRewriter rewriter = new(sm); + SyntaxNode newSource = rewriter.Visit(await sourceTree.GetRootAsync()); + if (newSource != await sourceTree.GetRootAsync()) + { + await File.WriteAllTextAsync(sourceTree.FilePath, newSource.ToFullString()); + } + UsingRewriter usingRewriter = new(); + var newUsingSource = usingRewriter.Visit(newSource); + if (newUsingSource != newSource) + { + await File.WriteAllTextAsync(sourceTree.FilePath, newUsingSource.ToFullString()); + } + } + + Console.WriteLine("References and using upgraded"); + return 0; + } + + static async Task UpgradeProcess(string processFile) + { + if (!File.Exists(processFile)) + { + Console.WriteLine($"Process file {processFile} does not exist. Please supply location of project with --process [path/to/project.csproj]"); + return 1; + } + + Console.WriteLine("Trying to upgrade process file"); + ProcessUpgrader parser = new(processFile); + parser.Upgrade(); + await parser.Write(); + var warnings = parser.GetWarnings(); + foreach (var warning in warnings) + { + Console.WriteLine(warning); + } + + Console.WriteLine(warnings.Any() ? "Process file upgraded with warnings. Review the warnings above and make sure that the process file is still valid." : "Process file upgraded"); + + return 0; + } +} diff --git a/cli-tools/altinn-app-cli/altinn-app-cli.csproj b/cli-tools/altinn-app-cli/altinn-app-cli.csproj new file mode 100644 index 000000000..4798b1dd1 --- /dev/null +++ b/cli-tools/altinn-app-cli/altinn-app-cli.csproj @@ -0,0 +1,42 @@ + + + + Exe + net6.0 + altinn_app_cli + latest + enable + enable + win-x64;linux-x64;osx-x64 + osx-x64 + altinn-app-cli + true + false + true + + + + + + + + + + + + + + + $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) + preview + altinn-app-cli + true + 10.0 + + + + + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) + + + diff --git a/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/TypesRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/TypesRewriter.cs new file mode 100644 index 000000000..8e7836248 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/TypesRewriter.cs @@ -0,0 +1,152 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace altinn_app_cli.v7Tov8.CodeRewriters; + +public class TypesRewriter: CSharpSyntaxRewriter +{ + private readonly SemanticModel semanticModel; + private readonly Dictionary fieldDescendantsMapping = new Dictionary() + { + {"Altinn.App.Core.Interface.IAppEvents", SyntaxFactory.IdentifierName("IAppEvents")}, + {"Altinn.App.Core.Interface.IApplication", SyntaxFactory.IdentifierName("IApplicationClient")}, + {"Altinn.App.Core.Interface.IAppResources", SyntaxFactory.IdentifierName("IAppResources")}, + {"Altinn.App.Core.Interface.IAuthentication", SyntaxFactory.IdentifierName("IAuthenticationClient")}, + {"Altinn.App.Core.Interface.IAuthorization", SyntaxFactory.IdentifierName("IAuthorizationClient")}, + {"Altinn.App.Core.Interface.IData", SyntaxFactory.IdentifierName("IDataClient")}, + {"Altinn.App.Core.Interface.IDSF", SyntaxFactory.IdentifierName("IPersonClient")}, + {"Altinn.App.Core.Interface.IER", SyntaxFactory.IdentifierName("IOrganizationClient")}, + {"Altinn.App.Core.Interface.IEvents", SyntaxFactory.IdentifierName("IEventsClient")}, + {"Altinn.App.Core.Interface.IInstance", SyntaxFactory.IdentifierName("IInstanceClient")}, + {"Altinn.App.Core.Interface.IInstanceEvent", SyntaxFactory.IdentifierName("IInstanceEventClient")}, + {"Altinn.App.Core.Interface.IPersonLookup", SyntaxFactory.IdentifierName("IPersonClient")}, + {"Altinn.App.Core.Interface.IPersonRetriever", SyntaxFactory.IdentifierName("IPersonClient")}, + {"Altinn.App.Core.Interface.IPrefill", SyntaxFactory.IdentifierName("IPrefill")}, + {"Altinn.App.Core.Interface.IProcess", SyntaxFactory.IdentifierName("IProcessClient")}, + {"Altinn.App.Core.Interface.IProfile", SyntaxFactory.IdentifierName("IProfileClient")}, + {"Altinn.App.Core.Interface.IRegister", SyntaxFactory.IdentifierName("IAltinnPartyClient")}, + {"Altinn.App.Core.Interface.ISecrets", SyntaxFactory.IdentifierName("ISecretsClient")}, + {"Altinn.App.Core.Interface.ITaskEvents", SyntaxFactory.IdentifierName("ITaskEvents")}, + {"Altinn.App.Core.Interface.IUserTokenProvider", SyntaxFactory.IdentifierName("IUserTokenProvider")} + }; + private readonly IEnumerable statementsToRemove = new List() + { + "app.UseDefaultSecurityHeaders();", + "app.UseRouting();", + "app.UseStaticFiles('/' + applicationId);", + "app.UseAuthentication();", + "app.UseAuthorization();", + "app.UseEndpoints(endpoints", + "app.UseHealthChecks(\"/health\");", + "app.UseAltinnAppCommonConfiguration();" + }; + + public TypesRewriter(SemanticModel semanticModel) + { + this.semanticModel = semanticModel; + } + + public override SyntaxNode? VisitFieldDeclaration(FieldDeclarationSyntax node) + { + return UpdateField(node); + } + + public override SyntaxNode? VisitParameter(ParameterSyntax node) + { + var parameterTypeName = node.Type; + if(parameterTypeName is null) + { + return node; + } + var parameterType = (ITypeSymbol?)semanticModel.GetSymbolInfo(parameterTypeName).Symbol; + if(parameterType?.ToString() != null && fieldDescendantsMapping.TryGetValue(parameterType.ToString()!, out var newType)) + { + var newTypeName = newType.WithLeadingTrivia(parameterTypeName.GetLeadingTrivia()).WithTrailingTrivia(parameterTypeName.GetTrailingTrivia()); + return node.ReplaceNode(parameterTypeName, newTypeName); + } + + return node; + } + + public override SyntaxNode? VisitGlobalStatement(GlobalStatementSyntax node) + { + if (node.Statement is LocalFunctionStatementSyntax localFunctionStatementSyntax) + { + if(localFunctionStatementSyntax.Identifier.Text == "Configure" && !localFunctionStatementSyntax.ParameterList.Parameters.Any() && localFunctionStatementSyntax.Body != null) + { + SyntaxTriviaList leadingTrivia = SyntaxFactory.TriviaList(); + SyntaxTriviaList trailingTrivia = SyntaxFactory.TriviaList(); + var newBody = SyntaxFactory.Block().WithoutLeadingTrivia().WithTrailingTrivia(localFunctionStatementSyntax.Body.GetTrailingTrivia()); + foreach (var childNode in localFunctionStatementSyntax.Body.ChildNodes()) + { + if(childNode is IfStatementSyntax ifStatementSyntax && ifStatementSyntax.Condition.ToString()!="app.Environment.IsDevelopment()") + { + newBody = AddStatementWithTrivia(newBody, ifStatementSyntax); + } + if(childNode is ExpressionStatementSyntax statementSyntax){ + leadingTrivia = statementSyntax.GetLeadingTrivia(); + trailingTrivia = statementSyntax.GetTrailingTrivia(); + if (!ShouldRemoveStatement(statementSyntax)) + { + newBody = AddStatementWithTrivia(newBody, statementSyntax); + } + } + if(childNode is LocalDeclarationStatementSyntax localDeclarationStatement) + { + newBody = AddStatementWithTrivia(newBody, localDeclarationStatement); + } + } + newBody = newBody.AddStatements(SyntaxFactory.ParseStatement("app.UseAltinnAppCommonConfiguration();").WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia)); + return node.ReplaceNode(localFunctionStatementSyntax.Body, newBody); + } + } + + return node; + } + + public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) + { + if (node.Identifier.Text == "FilterAsync" && + node.Parent is ClassDeclarationSyntax { BaseList: not null } classDeclarationSyntax + && classDeclarationSyntax.BaseList.Types.Any(x => x.Type.ToString() == "IProcessExclusiveGateway") + && node.ParameterList.Parameters.All(x => x.Type?.ToString() != "ProcessGatewayInformation")) + { + return node.AddParameterListParameters( + SyntaxFactory.Parameter(SyntaxFactory.Identifier("processGatewayInformation").WithLeadingTrivia(SyntaxFactory.ElasticSpace)).WithType(SyntaxFactory.ParseTypeName("ProcessGatewayInformation").WithLeadingTrivia(SyntaxFactory.ElasticSpace)) + ); + } + + return node; + } + + private FieldDeclarationSyntax UpdateField(FieldDeclarationSyntax node) + { + var variableTypeName = node.Declaration.Type; + var variableType = (ITypeSymbol?)semanticModel.GetSymbolInfo(variableTypeName).Symbol; + if(variableType?.ToString() != null && fieldDescendantsMapping.TryGetValue(variableType.ToString()!, out var newType)) + { + var newTypeName = newType.WithLeadingTrivia(variableTypeName.GetLeadingTrivia()).WithTrailingTrivia(variableTypeName.GetTrailingTrivia()); + node = node.ReplaceNode(variableTypeName, newTypeName); + Console.WriteLine($"Updated field {node.Declaration.Variables.First().Identifier.Text} from {variableType} to {newType}"); + } + return node; + } + + private bool ShouldRemoveStatement(StatementSyntax statementSyntax) + { + foreach (var statementToRemove in statementsToRemove) + { + var s = statementSyntax.ToString(); + if(s == statementToRemove || s.StartsWith(statementToRemove)) + { + return true; + } + } + return false; + } + private static BlockSyntax AddStatementWithTrivia(BlockSyntax block, StatementSyntax statement) + { + return block.AddStatements(statement).WithLeadingTrivia(statement.GetLeadingTrivia()).WithTrailingTrivia(statement.GetTrailingTrivia()); + } +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/UsingRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/UsingRewriter.cs new file mode 100644 index 000000000..1d692a3a7 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/UsingRewriter.cs @@ -0,0 +1,91 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace altinn_app_cli.v7Tov8.CodeRewriters; + +public class UsingRewriter: CSharpSyntaxRewriter +{ + private const string CommonInterfaceNamespace = "Altinn.App.Core.Interface"; + + private readonly Dictionary usingMappings = new Dictionary() + { + {"IAppEvents", "Altinn.App.Core.Internal.App"}, + {"IApplication", "Altinn.App.Core.Internal.App"}, + {"IAppResources", "Altinn.App.Core.Internal.App"}, + {"IAuthenticationClient", "Altinn.App.Core.Internal.Auth"}, + {"IAuthorizationClient", "Altinn.App.Core.Internal.Auth"}, + {"IDataClient", "Altinn.App.Core.Internal.Data"}, + {"IPersonClient", "Altinn.App.Core.Internal.Registers"}, + {"IOrganizationClient", "Altinn.App.Core.Internal.Registers"}, + {"IEventsClient", "Altinn.App.Core.Internal.Events"}, + {"IInstanceClient", "Altinn.App.Core.Internal.Instances"}, + {"IInstanceEventClient", "Altinn.App.Core.Internal.Instances"}, + {"IPrefill", "Altinn.App.Core.Internal.Prefill"}, + {"IProcessClient", "Altinn.App.Core.Internal.Process"}, + {"IProfileClient", "Altinn.App.Core.Internal.Profile"}, + {"IAltinnPartyClient", "Altinn.App.Core.Internal.Registers"}, + {"ISecretsClient", "Altinn.App.Core.Internal.Secrets"}, + {"ITaskEvents", "Altinn.App.Core.Internal.Process"}, + {"IUserTokenProvider", "Altinn.App.Core.Internal.Auth"}, + }; + + public override SyntaxNode? VisitCompilationUnit(CompilationUnitSyntax node) + { + foreach (var mapping in usingMappings) + { + if (HasFieldOfType(node, mapping.Key)) + { + node = AddUsing(node, mapping.Value); + } + } + + if (ImplementsIProcessExclusiveGateway(node)) + { + node = AddUsing(node, "Altinn.App.Core.Models.Process"); + } + + return RemoveOldUsing(node); + } + + private bool HasFieldOfType(CompilationUnitSyntax node, string typeName) + { + var fieldDecendants = node.DescendantNodes().OfType(); + return fieldDecendants.Any(f => f.Declaration.Type.ToString() == typeName); + } + + private bool ImplementsIProcessExclusiveGateway(CompilationUnitSyntax node) + { + var classDecendants = node.DescendantNodes().OfType(); + return classDecendants.Any(c => c.BaseList?.Types.Any(t => t.Type.ToString() == "IProcessExclusiveGateway") == true); + } + + private CompilationUnitSyntax AddUsing(CompilationUnitSyntax node, string usingString) + { + if (HasUsingDefined(node, usingString)) + { + return node; + } + var usingName = SyntaxFactory.ParseName(usingString); + var usingDirective = SyntaxFactory.UsingDirective(usingName).NormalizeWhitespace().WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed); + return node.AddUsings(usingDirective); + } + + private bool HasUsingDefined(CompilationUnitSyntax node, string usingName) + { + var usingDirectiveSyntaxes = node.DescendantNodes().OfType(); + return usingDirectiveSyntaxes.Any(u => u.Name?.ToString() == usingName); + } + + private CompilationUnitSyntax? RemoveOldUsing(CompilationUnitSyntax node) + { + var usingDirectiveSyntaxes = node.DescendantNodes().OfType(); + var usingDirectiveSyntax = usingDirectiveSyntaxes.FirstOrDefault(u => u.Name?.ToString() == CommonInterfaceNamespace); + if (usingDirectiveSyntax != null) + { + return node.RemoveNode(usingDirectiveSyntax, SyntaxRemoveOptions.KeepNoTrivia); + } + return node; + } + +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/ProcessRewriter/ProcessUpgrader.cs b/cli-tools/altinn-app-cli/v7Tov8/ProcessRewriter/ProcessUpgrader.cs new file mode 100644 index 000000000..e23be8615 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/ProcessRewriter/ProcessUpgrader.cs @@ -0,0 +1,168 @@ +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace altinn_app_cli.v7Tov8.ProcessRewriter; + +public class ProcessUpgrader +{ + private XDocument doc; + private readonly string processFile; + private readonly XNamespace newAltinnNs = "http://altinn.no/process"; + private readonly XNamespace origAltinnNs = "http://altinn.no"; + private readonly XNamespace bpmnNs = "http://www.omg.org/spec/BPMN/20100524/MODEL"; + private readonly IList warnings = new List(); + + public ProcessUpgrader(string processFile) + { + this.processFile = processFile; + var xmlString = File.ReadAllText(processFile); + xmlString = xmlString.Replace($"xmlns:altinn=\"{origAltinnNs}\"", $"xmlns:altinn=\"{newAltinnNs}\""); + doc = XDocument.Parse(xmlString); + } + + public void Upgrade() + { + var definitions = doc.Root; + var process = definitions?.Elements().Single(e => e.Name.LocalName == "process"); + var processElements = process?.Elements() ?? Enumerable.Empty(); + foreach (var processElement in processElements) + { + if (processElement.Name.LocalName == "task") + { + UpgradeTask(processElement); + } + else if (processElement.Name.LocalName == "sequenceFlow") + { + UpgradeSequenceFlow(processElement); + } + } + } + + private void UpgradeTask(XElement processElement) + { + var taskTypeAttr = processElement.Attribute(newAltinnNs + "tasktype"); + var taskType = taskTypeAttr?.Value; + if (taskType == null) + { + return; + } + XElement extensionElements = processElement.Element(bpmnNs + "extensionElements") ?? new XElement(bpmnNs + "extensionElements"); + XElement taskExtensionElement = extensionElements.Element(newAltinnNs + "taskExtension") ?? new XElement(newAltinnNs + "taskExtension"); + XElement taskTypeElement = new XElement(newAltinnNs + "taskType"); + taskTypeElement.Value = taskType; + taskExtensionElement.Add(taskTypeElement); + extensionElements.Add(taskExtensionElement); + processElement.Add(extensionElements); + taskTypeAttr?.Remove(); + if (taskType.Equals("confirmation")) + { + AddAction(processElement, "confirm"); + } + } + + private void UpgradeSequenceFlow(XElement processElement) + { + var flowTypeAttr = processElement.Attribute(newAltinnNs + "flowtype"); + flowTypeAttr?.Remove(); + if (flowTypeAttr?.Value != "AbandonCurrentReturnToNext") + { + return; + } + + var sourceRefAttr = processElement.Attribute("sourceRef"); + SetSequenceFlowAsDefaultIfGateway(sourceRefAttr?.Value!, processElement.Attribute("id")?.Value!); + var sourceTask = FollowGatewaysAndGetSourceTask(sourceRefAttr?.Value!); + AddAction(sourceTask, "reject"); + var conditionExpression = processElement.Elements().FirstOrDefault(e => e.Name.LocalName == "conditionExpression"); + if(conditionExpression == null) + { + conditionExpression = new XElement(bpmnNs + "conditionExpression"); + processElement.Add(conditionExpression); + } + conditionExpression.Value = "[\"equals\", [\"gatewayAction\"],\"reject\"]"; + warnings.Add($"SequenceFlow {processElement.Attribute("id")?.Value!} has flowtype {flowTypeAttr.Value} upgrade tool has tried to add reject action to source task. \nPlease verify that process flow is correct and that layoutfiels are updated to use ActionButtons\nRefere to docs.altinn.studio for how actions in v8 work"); + } + + private void SetSequenceFlowAsDefaultIfGateway(string elementRef, string sequenceFlowRef) + { + var sourceElement = doc.Root?.Elements().Single(e => e.Name.LocalName == "process").Elements().Single(e => e.Attribute("id")?.Value == elementRef); + if (sourceElement?.Name.LocalName == "exclusiveGateway") + { + if (sourceElement.Attribute("default") == null) + { + sourceElement.Add(new XAttribute("default", sequenceFlowRef)); + } + else + { + warnings.Add($"Default sequence flow already set for gateway {elementRef}. Process is most likely not correct. Please correct it manually and test it."); + } + } + } + + private XElement FollowGatewaysAndGetSourceTask(string sourceRef) + { + var processElement = doc.Root?.Elements().Single(e => e.Name.LocalName == "process"); + var sourceElement = processElement?.Elements().Single(e => e.Attribute("id")?.Value == sourceRef); + if (sourceElement?.Name.LocalName == "task") + { + return sourceElement; + } + + if (sourceElement?.Name.LocalName == "exclusiveGateway") + { + var incomingSequenceFlow = sourceElement.Elements().Single(e => e.Name.LocalName == "incoming").Value; + var incomingSequenceFlowRef = processElement?.Elements().Single(e => e.Attribute("id")!.Value == incomingSequenceFlow).Attribute("sourceRef")?.Value; + return FollowGatewaysAndGetSourceTask(incomingSequenceFlowRef!); + } + + throw new Exception("Unexpected element type"); + } + + private void AddAction(XElement sourceTask, string actionName) + { + var extensionElements = sourceTask.Element(bpmnNs + "extensionElements"); + if (extensionElements == null) + { + extensionElements = new XElement(bpmnNs + "extensionElements"); + sourceTask.Add(extensionElements); + } + + var taskExtensionElement = extensionElements.Element(newAltinnNs + "taskExtension"); + if (taskExtensionElement == null) + { + taskExtensionElement = new XElement(newAltinnNs + "taskExtension"); + extensionElements.Add(taskExtensionElement); + } + + var actions = taskExtensionElement.Element(newAltinnNs + "actions"); + if (actions == null) + { + actions = new XElement(newAltinnNs + "actions"); + taskExtensionElement.Add(actions); + } + if(actions.Elements().Any(e => e.Value == actionName)) + { + return; + } + var action = new XElement(newAltinnNs + "action"); + action.Value = actionName; + actions.Add(action); + } + + public async Task Write() + { + XmlWriterSettings xws = new XmlWriterSettings(); + xws.Async = true; + xws.OmitXmlDeclaration = false; + xws.Indent = true; + xws.Encoding = Encoding.UTF8; + await using XmlWriter xw = XmlWriter.Create(processFile, xws); + await doc.WriteToAsync(xw, CancellationToken.None); + } + + public IList GetWarnings() + { + return warnings; + } +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/ProjectChecks/ProjectChecks.cs b/cli-tools/altinn-app-cli/v7Tov8/ProjectChecks/ProjectChecks.cs new file mode 100644 index 000000000..4c4ddc73b --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/ProjectChecks/ProjectChecks.cs @@ -0,0 +1,71 @@ +using System.Xml.Linq; + +namespace altinn_app_cli.v7Tov8.ProjectChecks; + +public class ProjectChecks +{ + private XDocument doc; + + public ProjectChecks(string projectFilePath) + { + var xmlString = File.ReadAllText(projectFilePath); + doc = XDocument.Parse(xmlString); + } + + public bool SupportedSourceVersion() + { + var altinnAppCoreElements = GetAltinnAppCoreElement(); + var altinnAppApiElements = GetAltinnAppApiElement(); + if (altinnAppCoreElements == null || altinnAppApiElements == null) + { + return false; + } + + if (altinnAppApiElements.Select(apiElement => apiElement.Attribute("Version")?.Value).Any(altinnAppApiVersion => !SupportedSourceVersion(altinnAppApiVersion))) + { + return false; + } + + return altinnAppCoreElements.Select(coreElement => coreElement.Attribute("Version")?.Value).All(altinnAppCoreVersion => SupportedSourceVersion(altinnAppCoreVersion)); + + } + + private List? GetAltinnAppCoreElement() + { + return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Core").ToList(); + } + + private List? GetAltinnAppApiElement() + { + return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Api").ToList(); + } + + /// + /// Check that version is >=7.0.0 + /// + /// + /// + private bool SupportedSourceVersion(string? version) + { + if (version == null) + { + return false; + } + + var versionParts = version.Split('.'); + if (versionParts.Length < 3) + { + return false; + } + + if (int.TryParse(versionParts[0], out int major)) + { + if (major >= 7) + { + return true; + } + } + + return false; + } +} diff --git a/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs new file mode 100644 index 000000000..11def034d --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs @@ -0,0 +1,53 @@ +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace altinn_app_cli.v7Tov8.ProjectRewriters; + +public class ProjectFileRewriter +{ + private XDocument doc; + private readonly string projectFilePath; + private readonly string targetVersion; + + public ProjectFileRewriter(string projectFilePath, string targetVersion = "8.0.0") + { + this.projectFilePath = projectFilePath; + this.targetVersion = targetVersion; + var xmlString = File.ReadAllText(projectFilePath); + doc = XDocument.Parse(xmlString); + } + + public async Task Upgrade() + { + var altinnAppCoreElements = GetAltinnAppCoreElement(); + var altinnAppApiElements = GetAltinnAppApiElement(); + if (altinnAppCoreElements != null && altinnAppApiElements != null) + { + altinnAppCoreElements.ForEach(c => c.Attribute("Version")?.SetValue(targetVersion)); + altinnAppApiElements.ForEach(a => a.Attribute("Version")?.SetValue(targetVersion)); + await Save(); + } + } + + private List? GetAltinnAppCoreElement() + { + return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Core").ToList(); + } + + private List? GetAltinnAppApiElement() + { + return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Api").ToList(); + } + + private async Task Save() + { + XmlWriterSettings xws = new XmlWriterSettings(); + xws.Async = true; + xws.OmitXmlDeclaration = true; + xws.Indent = true; + xws.Encoding = Encoding.UTF8; + await using XmlWriter xw = XmlWriter.Create(projectFilePath, xws); + await doc.WriteToAsync(xw, CancellationToken.None); + } +} From 4da452705e476b4e15ec86da398188ba61c9b280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rnar=20=C3=98sttveit?= <47412359+bjosttveit@users.noreply.github.com> Date: Wed, 4 Oct 2023 11:11:20 +0200 Subject: [PATCH 17/46] Expression validation (#311) * frontend support for expression validation * parse expression validation config * parse config file * evaluate expressions for validation * add argv function * migrate from newtonsoft and fix parsing * improve error handling and fix numeric parse * started making shared tests * improved test runner * fix list of resolved keys * add tests and refactor jsondatamodel * update settings * add app-settings-rewriter to altinn-app-cli * fix upgrade appsettings * refactor JsonDataModel RemoveField and check deleteRow arg * add source and throw exception --- cli-tools/altinn-app-cli/Program.cs | 61 +++- .../AppSettingsRewriter.cs | 110 +++++++ .../Controllers/ResourceController.cs | 17 +- .../Configuration/AppSettings.cs | 26 +- .../Validation/ExpressionValidator.cs | 276 ++++++++++++++++++ .../Features/Validation/ValidationAppSI.cs | 27 +- .../Helpers/DataModel/DataModel.cs | 74 +++++ src/Altinn.App.Core/Helpers/IDataModel.cs | 5 + .../Implementation/AppResourcesSI.cs | 18 +- .../Implementation/DefaultTaskEvents.cs | 4 +- .../Internal/App/IAppResources.cs | 6 + .../Expressions/ExpressionEvaluator.cs | 58 +++- .../Expressions/ExpressionFunctionEnum.cs | 6 +- .../Models/Validation/ExpressionValidation.cs | 41 +++ .../Validation/FrontendSeverityConverter.cs | 42 +++ .../Validation/ValidationIssueSource.cs | 5 + .../Altinn.App.Core.Tests.csproj | 2 + .../Validators/ExpressionValidationTests.cs | 96 ++++++ .../hidden-field.json | 39 +++ .../hidden-page.json | 39 +++ .../many-errors.json | 101 +++++++ .../nested-repeating-hidden-row.json | 179 ++++++++++++ .../nested-repeating-hidden.json | 191 ++++++++++++ .../override.json | 51 ++++ .../repeating-hidden-row.json | 72 +++++ .../repeating-hidden.json | 72 +++++ .../repeating.json | 73 +++++ .../single-field-equals.json | 54 ++++ .../warning.json | 44 +++ .../Helpers/JsonDataModel.cs | 204 +++++++++++-- .../CommonTests/ContextListRoot.cs | 5 +- .../CommonTests/ExpressionTestCaseRoot.cs | 3 +- .../LayoutExpressions/TestDataModel.cs | 10 +- 33 files changed, 1951 insertions(+), 60 deletions(-) create mode 100644 cli-tools/altinn-app-cli/v7Tov8/AppSettingsRewriter/AppSettingsRewriter.cs create mode 100644 src/Altinn.App.Core/Features/Validation/ExpressionValidator.cs create mode 100644 src/Altinn.App.Core/Models/Validation/ExpressionValidation.cs create mode 100644 src/Altinn.App.Core/Models/Validation/FrontendSeverityConverter.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/ExpressionValidationTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/hidden-field.json create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/hidden-page.json create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/many-errors.json create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden-row.json create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden.json create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/override.json create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden-row.json create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden.json create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating.json create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/single-field-equals.json create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/warning.json diff --git a/cli-tools/altinn-app-cli/Program.cs b/cli-tools/altinn-app-cli/Program.cs index 17a257d7a..6402f4d71 100644 --- a/cli-tools/altinn-app-cli/Program.cs +++ b/cli-tools/altinn-app-cli/Program.cs @@ -1,5 +1,7 @@ using System.CommandLine; +using System.CommandLine.Invocation; using System.Reflection; +using altinn_app_cli.v7Tov8.AppSettingsRewriter; using altinn_app_cli.v7Tov8.CodeRewriters; using altinn_app_cli.v7Tov8.ProcessRewriter; using altinn_app_cli.v7Tov8.ProjectChecks; @@ -18,27 +20,42 @@ static async Task Main(string[] args) var projectFolderOption = new Option(name: "--folder", description: "The project folder to read", getDefaultValue: () => "CurrentDirectory"); var projectFileOption = new Option(name: "--project", description: "The project file to read relative to --folder", getDefaultValue: () => "App/App.csproj"); var processFileOption = new Option(name: "--process", description: "The process file to read relative to --folder", getDefaultValue: () => "App/config/process/process.bpmn"); + var appSettingsFolderOption = new Option(name: "--appsettings-folder", description: "The folder where the appsettings.*.json files are located", getDefaultValue: () => "App"); var targetVersionOption = new Option(name: "--target-version", description: "The target version to upgrade to", getDefaultValue: () => "8.0.0-preview.9"); var skipCsprojUpgradeOption = new Option(name: "--skip-csproj-upgrade", description: "Skip csproj file upgrade", getDefaultValue: () => false); var skipCodeUpgradeOption = new Option(name: "--skip-code-upgrade", description: "Skip code upgrade", getDefaultValue: () => false); var skipProcessUpgradeOption = new Option(name: "--skip-process-upgrade", description: "Skip process file upgrade", getDefaultValue: () => false); + var skipAppSettingsUpgradeOption = new Option(name: "--skip-appsettings-upgrade", description: "Skip appsettings file upgrade", getDefaultValue: () => false); var rootCommand = new RootCommand("Command line interface for working with Altinn 3 Applications"); var upgradeCommand = new Command("upgrade", "Upgrade an app from v7 to v8") { projectFolderOption, projectFileOption, processFileOption, + appSettingsFolderOption, targetVersionOption, skipCsprojUpgradeOption, skipCodeUpgradeOption, skipProcessUpgradeOption, + skipAppSettingsUpgradeOption, }; rootCommand.AddCommand(upgradeCommand); var versionCommand = new Command("version", "Print version of altinn-app-cli"); rootCommand.AddCommand(versionCommand); - upgradeCommand.SetHandler(async (projectFolder, projectFile, processFile, targetVersion, skipCodeUpgrade, skipProcessUpgrade, skipCsprojUpgrade) => + upgradeCommand.SetHandler( + async (InvocationContext context) => { + var projectFolder = context.ParseResult.GetValueForOption(projectFolderOption)!; + var projectFile = context.ParseResult.GetValueForOption(projectFileOption)!; + var processFile = context.ParseResult.GetValueForOption(processFileOption)!; + var appSettingsFolder = context.ParseResult.GetValueForOption(appSettingsFolderOption)!; + var targetVersion = context.ParseResult.GetValueForOption(targetVersionOption)!; + var skipCodeUpgrade = context.ParseResult.GetValueForOption(skipCodeUpgradeOption)!; + var skipProcessUpgrade = context.ParseResult.GetValueForOption(skipProcessUpgradeOption)!; + var skipCsprojUpgrade = context.ParseResult.GetValueForOption(skipCsprojUpgradeOption)!; + var skipAppSettingsUpgrade = context.ParseResult.GetValueForOption(skipAppSettingsUpgradeOption)!; + if (projectFolder == "CurrentDirectory") { projectFolder = Directory.GetCurrentDirectory(); @@ -63,11 +80,13 @@ static async Task Main(string[] args) { projectFile = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, projectFile); processFile = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, processFile); + appSettingsFolder = Path.Combine(Directory.GetCurrentDirectory(), projectFolder, appSettingsFolder); } else { projectFile = Path.Combine(projectFolder, projectFile); processFile = Path.Combine(projectFolder, processFile); + appSettingsFolder = Path.Combine(projectFolder, appSettingsFolder); } var projectChecks = new ProjectChecks(projectFile); @@ -77,6 +96,7 @@ static async Task Main(string[] args) returnCode = 2; return; } + if (!skipCsprojUpgrade) { returnCode = await UpgradeNugetVersions(projectFile, targetVersion); @@ -92,6 +112,11 @@ static async Task Main(string[] args) returnCode = await UpgradeProcess(processFile); } + if (!skipAppSettingsUpgrade && returnCode == 0) + { + returnCode = await UpgradeAppSettings(appSettingsFolder); + } + if (returnCode == 0) { Console.WriteLine("Upgrade completed without errors. Please verify that the application is still working as expected."); @@ -100,8 +125,9 @@ static async Task Main(string[] args) { Console.WriteLine("Upgrade completed with errors. Please check for errors in the log above."); } - }, - projectFolderOption, projectFileOption, processFileOption, targetVersionOption, skipCodeUpgradeOption, skipProcessUpgradeOption, skipCsprojUpgradeOption); + } + ); + versionCommand.SetHandler(() => { var version = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion ?? "Unknown"; @@ -188,4 +214,33 @@ static async Task UpgradeProcess(string processFile) return 0; } + + static async Task UpgradeAppSettings(string appSettingsFolder) + { + if (!Directory.Exists(appSettingsFolder)) + { + Console.WriteLine($"App settings folder {appSettingsFolder} does not exist. Please supply location with --appsettings-folder [path/to/appsettings]"); + return 1; + } + + if (Directory.GetFiles(appSettingsFolder, AppSettingsRewriter.APP_SETTINGS_FILE_PATTERN).Count() == 0) + { + Console.WriteLine($"No appsettings*.json files found in {appSettingsFolder}"); + return 1; + } + + Console.WriteLine("Trying to upgrade appsettings*.json files"); + AppSettingsRewriter rewriter = new(appSettingsFolder); + rewriter.Upgrade(); + await rewriter.Write(); + var warnings = rewriter.GetWarnings(); + foreach (var warning in warnings) + { + Console.WriteLine(warning); + } + + Console.WriteLine(warnings.Any() ? "AppSettings files upgraded with warnings. Review the warnings above and make sure that the appsettings files are still valid." : "AppSettings files upgraded"); + + return 0; + } } diff --git a/cli-tools/altinn-app-cli/v7Tov8/AppSettingsRewriter/AppSettingsRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/AppSettingsRewriter/AppSettingsRewriter.cs new file mode 100644 index 000000000..860d2b1b3 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/AppSettingsRewriter/AppSettingsRewriter.cs @@ -0,0 +1,110 @@ + +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace altinn_app_cli.v7Tov8.AppSettingsRewriter; + + +/// +/// Rewrites the appsettings.*.json files +/// +public class AppSettingsRewriter +{ + /// + /// The pattern used to search for appsettings.*.json files + /// + public static readonly string APP_SETTINGS_FILE_PATTERN = "appsettings*.json"; + + private Dictionary appSettingsJsonCollection; + + private readonly IList warnings = new List(); + + /// + /// Initializes a new instance of the class. + /// + public AppSettingsRewriter(string appSettingsFolder) + { + appSettingsJsonCollection = new Dictionary(); + foreach (var file in Directory.GetFiles(appSettingsFolder, APP_SETTINGS_FILE_PATTERN)) + { + var json = File.ReadAllText(file); + var appSettingsJson = JsonNode.Parse(json); + if (appSettingsJson is not JsonObject appSettingsJsonObject) + { + warnings.Add($"Unable to parse AppSettings file {file} as a json object, skipping"); + continue; + } + + this.appSettingsJsonCollection.Add(file, appSettingsJsonObject); + } + } + + /// + /// Gets the warnings + /// + public IList GetWarnings() + { + return warnings; + } + + /// + /// Upgrades the appsettings.*.json files + /// + public void Upgrade() + { + foreach ((var fileName, var appSettingsJson) in appSettingsJsonCollection) + { + RewriteRemoveHiddenDataSetting(fileName, appSettingsJson); + } + } + + /// + /// Writes the appsettings.*.json files + /// + public async Task Write() + { + var tasks = appSettingsJsonCollection.Select(async appSettingsFiles => + { + appSettingsFiles.Deconstruct(out var fileName, out var appSettingsJson); + + JsonSerializerOptions options = new JsonSerializerOptions + { + WriteIndented = true, + }; + await File.WriteAllTextAsync(fileName, appSettingsJson.ToJsonString(options)); + }); + + await Task.WhenAll(tasks); + } + + private void RewriteRemoveHiddenDataSetting(string fileName, JsonObject settings) + { + // Look for "AppSettings" object + settings.TryGetPropertyValue("AppSettings", out var appSettingsNode); + if (appSettingsNode is not JsonObject appSettingsObject) + { + // No "AppSettings" object found, nothing to change + return; + } + + // Look for "RemoveHiddenDataPreview" property + appSettingsObject.TryGetPropertyValue("RemoveHiddenDataPreview", out var removeHiddenDataPreviewNode); + if (removeHiddenDataPreviewNode is not JsonValue removeHiddenDataPreviewValue) + { + // No "RemoveHiddenDataPreview" property found, nothing to change + return; + } + + // Get value of "RemoveHiddenDataPreview" property + if (!removeHiddenDataPreviewValue.TryGetValue(out var removeHiddenDataValue)) + { + warnings.Add($"RemoveHiddenDataPreview has unexpected value {removeHiddenDataPreviewValue.ToJsonString()} in {fileName}, expected a boolean"); + return; + } + + appSettingsObject.Remove("RemoveHiddenDataPreview"); + appSettingsObject.Add("RemoveHiddenData", removeHiddenDataValue); + appSettingsObject.Add("RequiredValidation", removeHiddenDataValue); + + } +} diff --git a/src/Altinn.App.Api/Controllers/ResourceController.cs b/src/Altinn.App.Api/Controllers/ResourceController.cs index df01f78c1..7029e3b5e 100644 --- a/src/Altinn.App.Api/Controllers/ResourceController.cs +++ b/src/Altinn.App.Api/Controllers/ResourceController.cs @@ -265,8 +265,23 @@ public async Task GetFooterLayout(string org, string app) { return NoContent(); } - + return Ok(layout); } + + /// + /// Get validation configuration file. + /// + /// The application owner short name + /// The application name + /// Unique identifier of the model to fetch validations for. + /// The validation configuration file as json. + [HttpGet] + [Route("{org}/{app}/api/validationconfig/{id}")] + public ActionResult GetValidationConfiguration(string org, string app, string id) + { + string? validationConfiguration = _appResourceService.GetValidationConfiguration(id); + return Ok(validationConfiguration); + } } } diff --git a/src/Altinn.App.Core/Configuration/AppSettings.cs b/src/Altinn.App.Core/Configuration/AppSettings.cs index 9974f7960..ab34f341d 100644 --- a/src/Altinn.App.Core/Configuration/AppSettings.cs +++ b/src/Altinn.App.Core/Configuration/AppSettings.cs @@ -23,6 +23,11 @@ public class AppSettings /// public const string JSON_SCHEMA_FILENAME = "schema.json"; + /// + /// Constant for the location of validation configuration file + /// + public const string VALIDATION_CONFIG_FILENAME = "validation.json"; + /// /// The app configuration baseUrl where files are stored in the container /// @@ -83,7 +88,7 @@ public class AppSettings /// public string LayoutSetsFileName { get; set; } = "layout-sets.json"; - /// + /// /// Gets or sets the name of the layout setting file name /// public string FooterFileName { get; set; } = "footer.json"; @@ -103,6 +108,11 @@ public class AppSettings /// public string JsonSchemaFileName { get; set; } = JSON_SCHEMA_FILENAME; + /// + /// Gets or sets The JSON schema file name + /// + public string ValidationConfigurationFileName { get; set; } = VALIDATION_CONFIG_FILENAME; + /// /// Gets or sets the filename for application meta data /// @@ -214,8 +224,18 @@ public string GetResourceFolder() public string AppVersion { get; set; } /// - /// Enable the preview functionality to load layout in backend and remove data from hidden components before validation and task completion + /// Enable the functionality to load layout in backend and remove data from hidden components before task completion + /// + public bool RemoveHiddenData { get; set; } = false; + + /// + /// Enable the functionality to load layout in backend and validate required fields as defined in the layout + /// + public bool RequiredValidation { get; set; } = false; + + /// + /// Enable the functionality to run expression validation in backend /// - public bool RemoveHiddenDataPreview { get; set; } = false; + public bool ExpressionValidation { get; set; } = false; } } diff --git a/src/Altinn.App.Core/Features/Validation/ExpressionValidator.cs b/src/Altinn.App.Core/Features/Validation/ExpressionValidator.cs new file mode 100644 index 000000000..28fbeb047 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/ExpressionValidator.cs @@ -0,0 +1,276 @@ +using System.Text.Json; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Models.Validation; +using Microsoft.Extensions.Logging; + + +namespace Altinn.App.Core.Features.Validation +{ + /// + /// Validates form data against expression validations + /// + public static class ExpressionValidator + { + /// + public static IEnumerable Validate(string dataType, IAppResources appResourceService, IDataModelAccessor dataModel, LayoutEvaluatorState evaluatorState, ILogger logger) + { + var rawValidationConfig = appResourceService.GetValidationConfiguration(dataType); + if (rawValidationConfig == null) + { + // No validation configuration exists for this data type + return new List(); + } + + var validationConfig = JsonDocument.Parse(rawValidationConfig).RootElement; + return Validate(validationConfig, dataModel, evaluatorState, logger); + } + + /// + public static IEnumerable Validate(JsonElement validationConfig, IDataModelAccessor dataModel, LayoutEvaluatorState evaluatorState, ILogger logger) + { + var validationIssues = new List(); + var expressionValidations = ParseExpressionValidationConfig(validationConfig, logger); + foreach (var validationObject in expressionValidations) + { + var baseField = validationObject.Key; + var resolvedFields = dataModel.GetResolvedKeys(baseField); + var validations = validationObject.Value; + foreach (var resolvedField in resolvedFields) + { + var positionalArguments = new[] { resolvedField }; + foreach (var validation in validations) + { + try + { + if (validation.Condition == null) + { + continue; + } + + var isInvalid = ExpressionEvaluator.EvaluateExpression(evaluatorState, validation.Condition, null, positionalArguments); + if (isInvalid is not bool) + { + throw new ArgumentException($"Validation condition for {resolvedField} did not evaluate to a boolean"); + } + if ((bool)isInvalid) + { + var validationIssue = new ValidationIssue + { + Field = resolvedField, + Severity = validation.Severity ?? ValidationIssueSeverity.Error, + CustomTextKey = validation.Message, + Code = validation.Message, + Source = ValidationIssueSources.Expression, + }; + validationIssues.Add(validationIssue); + } + } + catch + { + logger.LogError($"Error while evaluating expression validation for {resolvedField}"); + throw; + } + } + } + } + + + return validationIssues; + } + + private static RawExpressionValidation? ResolveValidationDefinition(string name, JsonElement definition, Dictionary resolvedDefinitions, ILogger logger) + { + var resolvedDefinition = new RawExpressionValidation(); + var rawDefinition = definition.Deserialize(new JsonSerializerOptions + { + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }); + if (rawDefinition == null) + { + logger.LogError($"Validation definition {name} could not be parsed"); + return null; + } + if (rawDefinition.Ref != null) + { + var reference = resolvedDefinitions.GetValueOrDefault(rawDefinition.Ref); + if (reference == null) + { + logger.LogError($"Could not resolve reference {rawDefinition.Ref} for validation {name}"); + return null; + + } + resolvedDefinition.Message = reference.Message; + resolvedDefinition.Condition = reference.Condition; + resolvedDefinition.Severity = reference.Severity; + } + + if (rawDefinition.Message != null) + { + resolvedDefinition.Message = rawDefinition.Message; + } + + if (rawDefinition.Condition != null) + { + resolvedDefinition.Condition = rawDefinition.Condition; + } + + if (rawDefinition.Severity != null) + { + resolvedDefinition.Severity = rawDefinition.Severity; + } + + if (resolvedDefinition.Message == null) + { + logger.LogError($"Validation {name} is missing message"); + return null; + } + + if (resolvedDefinition.Condition == null) + { + logger.LogError($"Validation {name} is missing condition"); + return null; + } + + return resolvedDefinition; + } + + private static ExpressionValidation? ResolveExpressionValidation(string field, JsonElement definition, Dictionary resolvedDefinitions, ILogger logger) + { + + var rawExpressionValidatıon = new RawExpressionValidation(); + + if (definition.ValueKind == JsonValueKind.String) + { + var stringReference = definition.GetString(); + if (stringReference == null) + { + logger.LogError($"Could not resolve null reference for validation for field {field}"); + return null; + } + var reference = resolvedDefinitions.GetValueOrDefault(stringReference); + if (reference == null) + { + logger.LogError($"Could not resolve reference {stringReference} for validation for field {field}"); + return null; + } + rawExpressionValidatıon.Message = reference.Message; + rawExpressionValidatıon.Condition = reference.Condition; + rawExpressionValidatıon.Severity = reference.Severity; + } + else + { + var expressionDefinition = definition.Deserialize(new JsonSerializerOptions + { + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }); + if (expressionDefinition == null) + { + logger.LogError($"Validation for field {field} could not be parsed"); + return null; + } + + if (expressionDefinition.Ref != null) + { + var reference = resolvedDefinitions.GetValueOrDefault(expressionDefinition.Ref); + if (reference == null) + { + logger.LogError($"Could not resolve reference {expressionDefinition.Ref} for validation for field {field}"); + return null; + + } + rawExpressionValidatıon.Message = reference.Message; + rawExpressionValidatıon.Condition = reference.Condition; + rawExpressionValidatıon.Severity = reference.Severity; + } + + if (expressionDefinition.Message != null) + { + rawExpressionValidatıon.Message = expressionDefinition.Message; + } + + if (expressionDefinition.Condition != null) + { + rawExpressionValidatıon.Condition = expressionDefinition.Condition; + } + + if (expressionDefinition.Severity != null) + { + rawExpressionValidatıon.Severity = expressionDefinition.Severity; + } + } + + if (rawExpressionValidatıon.Message == null) + { + logger.LogError($"Validation for field {field} is missing message"); + return null; + } + + if (rawExpressionValidatıon.Condition == null) + { + logger.LogError($"Validation for field {field} is missing condition"); + return null; + } + + var expressionValidation = new ExpressionValidation + { + Message = rawExpressionValidatıon.Message, + Condition = rawExpressionValidatıon.Condition, + Severity = rawExpressionValidatıon.Severity ?? ValidationIssueSeverity.Error, + }; + + return expressionValidation; + } + + private static Dictionary> ParseExpressionValidationConfig(JsonElement expressionValidationConfig, ILogger logger) + { + var expressionValidationDefinitions = new Dictionary(); + JsonElement definitionsObject; + var hasDefinitions = expressionValidationConfig.TryGetProperty("definitions", out definitionsObject); + if (hasDefinitions) + { + foreach (var definitionObject in definitionsObject.EnumerateObject()) + { + var name = definitionObject.Name; + var definition = definitionObject.Value; + var resolvedDefinition = ResolveValidationDefinition(name, definition, expressionValidationDefinitions, logger); + if (resolvedDefinition == null) + { + logger.LogError($"Validation definition {name} could not be resolved"); + continue; + } + expressionValidationDefinitions[name] = resolvedDefinition; + } + } + var expressionValidations = new Dictionary>(); + JsonElement validationsObject; + var hasValidations = expressionValidationConfig.TryGetProperty("validations", out validationsObject); + if (hasValidations) + { + foreach (var validationArray in validationsObject.EnumerateObject()) + { + var field = validationArray.Name; + var validations = validationArray.Value; + foreach (var validation in validations.EnumerateArray()) + { + if (!expressionValidations.ContainsKey(field)) + { + expressionValidations[field] = new List(); + } + var resolvedExpressionValidation = ResolveExpressionValidation(field, validation, expressionValidationDefinitions, logger); + if (resolvedExpressionValidation == null) + { + logger.LogError($"Validation for field {field} could not be resolved"); + continue; + } + expressionValidations[field].Add(resolvedExpressionValidation); + } + } + } + return expressionValidations; + } + } +} diff --git a/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs b/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs index 55a85a6a1..cfb9028e1 100644 --- a/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs +++ b/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs @@ -1,4 +1,5 @@ using Altinn.App.Core.Configuration; +using Altinn.App.Core.Helpers.DataModel; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; @@ -228,18 +229,32 @@ public async Task> ValidateDataElement(Instance instance, object data = await _dataClient.GetFormData( instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, Guid.Parse(dataElement.Id)); - if (_appSettings.RemoveHiddenDataPreview) + LayoutEvaluatorState? evaluationState = null; + + // Remove hidden data before validation + if (_appSettings.RequiredValidation || _appSettings.ExpressionValidation) { + var layoutSet = _appResourcesService.GetLayoutSetForTask(dataType.TaskId); - var evaluationState = await _layoutEvaluatorStateInitializer.Init(instance, data, layoutSet?.Id); - // Remove hidden data before validation, set rows to null to preserve indices + evaluationState = await _layoutEvaluatorStateInitializer.Init(instance, data, layoutSet?.Id); LayoutEvaluator.RemoveHiddenData(evaluationState, RowRemovalOption.SetToNull); - // Evaluate expressions in layout and validate that all required data is included and that maxLength - // is respected on groups - var layoutErrors = LayoutEvaluator.RunLayoutValidationsForRequired(evaluationState, dataElement.Id); + } + + // Evaluate expressions in layout and validate that all required data is included and that maxLength + // is respected on groups + if (_appSettings.RequiredValidation) + { + var layoutErrors = LayoutEvaluator.RunLayoutValidationsForRequired(evaluationState!, dataElement.Id); messages.AddRange(layoutErrors); } + // Run expression validations + if (_appSettings.ExpressionValidation) + { + var expressionErrors = ExpressionValidator.Validate(dataType.Id, _appResourcesService, new DataModel(data), evaluationState!, _logger); + messages.AddRange(expressionErrors); + } + // Run Standard mvc validation using the System.ComponentModel.DataAnnotations ModelStateDictionary dataModelValidationResults = new ModelStateDictionary(); var actionContext = new ActionContext( diff --git a/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs b/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs index 9aed92f8d..ec45626c9 100644 --- a/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs +++ b/src/Altinn.App.Core/Helpers/DataModel/DataModel.cs @@ -90,6 +90,80 @@ public DataModel(object serviceModel) return GetModelDataRecursive(keys, index + 1, elementAt, indicies.Length > 0 ? indicies.Slice(1) : indicies); } + /// + public string[] GetResolvedKeys(string key) + { + if (_serviceModel is null) + { + return new string[0]; + } + + var keyParts = key.Split('.'); + return GetResolvedKeysRecursive(keyParts, _serviceModel); + } + + internal static string JoinFieldKeyParts(string? currentKey, string? key) + { + if (String.IsNullOrEmpty(currentKey)) + { + return key ?? ""; + } + if (String.IsNullOrEmpty(key)) + { + return currentKey ?? ""; + } + + return currentKey + "." + key; + } + + private string[] GetResolvedKeysRecursive(string[] keyParts, object currentModel, int currentIndex = 0, string currentKey = "") + { + if (currentModel is null) + { + return new string[0]; + } + + if (currentIndex == keyParts.Length) + { + return new[] { currentKey }; + } + + var (key, groupIndex) = ParseKeyPart(keyParts[currentIndex]); + var prop = currentModel?.GetType().GetProperties().FirstOrDefault(p => IsPropertyWithJsonName(p, key)); + var childModel = prop?.GetValue(currentModel); + if (childModel is null) + { + return new string[0]; + } + + if (childModel is not string && childModel is System.Collections.IEnumerable childModelList) + { + // childModel is an array + if (groupIndex is null) + { + // Index not specified, recurse on all elements + int i = 0; + var resolvedKeys = new List(); + foreach (var child in childModelList) + { + var newResolvedKeys = GetResolvedKeysRecursive(keyParts, child, currentIndex + 1, JoinFieldKeyParts(currentKey, key + "[" + i + "]")); + resolvedKeys.AddRange(newResolvedKeys); + i++; + } + return resolvedKeys.ToArray(); + } + else + { + // Index specified, recurse on that element + return GetResolvedKeysRecursive(keyParts, childModel, currentIndex + 1, JoinFieldKeyParts(currentKey, key + "[" + groupIndex + "]")); + } + } + + // Otherwise, just recurse + return GetResolvedKeysRecursive(keyParts, childModel, currentIndex + 1, JoinFieldKeyParts(currentKey, key)); + + } + private static object? GetElementAt(System.Collections.IEnumerable enumerable, int index) { // Return the element with index = groupIndex (could not find anohter way to get the n'th element in non generic enumerable) diff --git a/src/Altinn.App.Core/Helpers/IDataModel.cs b/src/Altinn.App.Core/Helpers/IDataModel.cs index 2f267da9e..9d720b1aa 100644 --- a/src/Altinn.App.Core/Helpers/IDataModel.cs +++ b/src/Altinn.App.Core/Helpers/IDataModel.cs @@ -26,6 +26,11 @@ public interface IDataModelAccessor /// int? GetModelDataCount(string key, ReadOnlySpan indicies = default); + /// + /// Get all of the resoved keys (including all possible indexes) from a data model key + /// + string[] GetResolvedKeys(string key); + /// /// Return a full dataModelBiding from a context aware binding by adding indicies /// diff --git a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs index 4d5a33dcc..980d1ebfb 100644 --- a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs +++ b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs @@ -119,7 +119,7 @@ public Application GetApplication() Show = applicationMetadata.OnEntry.Show }; } - + return application; } catch (AggregateException ex) @@ -463,5 +463,21 @@ private byte[] ReadFileContentsFromLegalPath(string legalPath, string filePath) return filedata; } + + /// + public string? GetValidationConfiguration(string modelId) + { + string legalPath = $"{_settings.AppBasePath}{_settings.ModelsFolder}"; + string filename = $"{legalPath}{modelId}.{_settings.ValidationConfigurationFileName}"; + PathHelper.EnsureLegalPath(legalPath, filename); + + string? filedata = null; + if (File.Exists(filename)) + { + filedata = File.ReadAllText(filename, Encoding.UTF8); + } + + return filedata; + } } } diff --git a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs index c5759b312..5e09d3e4c 100644 --- a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs +++ b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs @@ -165,7 +165,7 @@ public async Task OnEndProcessTask(string endEvent, Instance instance) private async Task RunRemoveHiddenData(Instance instance, Guid instanceGuid, List? dataTypesToLock) { - if (_appSettings?.RemoveHiddenDataPreview == true) + if (_appSettings?.RemoveHiddenData == true) { await RemoveHiddenData(instance, instanceGuid, dataTypesToLock); } @@ -269,7 +269,7 @@ private async Task RemoveHiddenData(Instance instance, Guid instanceGuid, List $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) - preview + preview.0 altinn-app-cli true 10.0 diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index bb16215f2..3bb643875 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -13,7 +13,7 @@ git https://github.com/Altinn/app-lib-dotnet true - true + enable {E8F29FE8-6B62-41F1-A08C-2A318DD08BB4} diff --git a/src/Altinn.App.Api/Controllers/AuthorizationController.cs b/src/Altinn.App.Api/Controllers/AuthorizationController.cs index a63839160..d3a5f2eb5 100644 --- a/src/Altinn.App.Api/Controllers/AuthorizationController.cs +++ b/src/Altinn.App.Api/Controllers/AuthorizationController.cs @@ -1,3 +1,5 @@ +#nullable enable + using Altinn.App.Core.Configuration; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Auth; @@ -70,7 +72,7 @@ public async Task GetCurrentParty(bool returnPartyObject = false) } } - string cookieValue = Request.Cookies[_settings.GetAltinnPartyCookieName]; + string? cookieValue = Request.Cookies[_settings.GetAltinnPartyCookieName]; if (!int.TryParse(cookieValue, out int partyIdFromCookie)) { partyIdFromCookie = 0; diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index 274fb1725..1f168beb5 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -1,3 +1,5 @@ +#nullable enable + using System.Net; using System.Security.Claims; using Altinn.App.Api.Helpers.RequestHandling; @@ -30,6 +32,7 @@ namespace Altinn.App.Api.Controllers /// The data controller handles creation, update, validation and calculation of data elements. /// [AutoValidateAntiforgeryTokenIfAuthCookie] + [ApiController] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data")] public class DataController : ControllerBase @@ -113,18 +116,13 @@ public async Task Create( [FromRoute] Guid instanceGuid, [FromQuery] string dataType) { - if (string.IsNullOrWhiteSpace(dataType)) - { - return BadRequest("Element type must be provided."); - } - /* The Body of the request is read much later when it has been made sure it is worth it. */ try { Application application = await _appMetadata.GetApplicationMetadata(); - - DataType dataTypeFromMetadata = application.DataTypes.FirstOrDefault(e => e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase)); + + DataType? dataTypeFromMetadata = application.DataTypes.FirstOrDefault(e => e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase)); if (dataTypeFromMetadata == null) { @@ -158,7 +156,7 @@ public async Task Create( (bool validationRestrictionSuccess, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataTypeFromMetadata); if (!validationRestrictionSuccess) { - return new BadRequestObjectResult(await GetErrorDetails(errors)); + return BadRequest(await GetErrorDetails(errors)); } StreamContent streamContent = Request.CreateContentStream(); @@ -175,11 +173,11 @@ public async Task Create( Description = errorMessage }; _logger.LogError(errorMessage); - return new BadRequestObjectResult(await GetErrorDetails(new List { error })); + return BadRequest(await GetErrorDetails(new List { error })); } bool parseSuccess = Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues); - string filename = parseSuccess ? DataRestrictionValidation.GetFileNameFromHeader(headerValues) : string.Empty; + string? filename = parseSuccess ? DataRestrictionValidation.GetFileNameFromHeader(headerValues) : null; IEnumerable fileAnalysisResults = new List(); if (FileAnalysisEnabledForDataType(dataTypeFromMetadata)) @@ -196,7 +194,7 @@ public async Task Create( if (!fileValidationSuccess) { - return new BadRequestObjectResult(await GetErrorDetails(validationIssues)); + return BadRequest(await GetErrorDetails(validationIssues)); } fileStream.Seek(0, SeekOrigin.Begin); @@ -258,7 +256,7 @@ public async Task Get( return NotFound($"Did not find instance {instance}"); } - DataElement dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { @@ -319,7 +317,7 @@ public async Task Put( return Conflict($"Cannot update data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}"); } - DataElement dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { @@ -332,19 +330,19 @@ public async Task Put( if (appLogic == null) { - _logger.LogError($"Could not determine if {dataType} requires app logic for application {org}/{app}"); + _logger.LogError("Could not determine if {dataType} requires app logic for application {org}/{app}", dataType, org, app); return BadRequest($"Could not determine if data type {dataType} requires application logic."); } - else if ((bool)appLogic) + else if (appLogic == true) { return await PutFormData(org, app, instance, dataGuid, dataType); } - DataType dataTypeFromMetadata = (await _appMetadata.GetApplicationMetadata()).DataTypes.FirstOrDefault(e => e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase)); + DataType? dataTypeFromMetadata = (await _appMetadata.GetApplicationMetadata()).DataTypes.FirstOrDefault(e => e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase)); (bool validationRestrictionSuccess, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataTypeFromMetadata); if (!validationRestrictionSuccess) { - return new BadRequestObjectResult(await GetErrorDetails(errors)); + return BadRequest(await GetErrorDetails(errors)); } return await PutBinaryData(instanceOwnerPartyId, instanceGuid, dataGuid); @@ -386,7 +384,7 @@ public async Task Delete( return Conflict($"Cannot delete data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}"); } - DataElement dataElement = instance.Data.Find(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.Find(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { @@ -421,21 +419,19 @@ private ActionResult ExceptionResponse(Exception exception, string message) { _logger.LogError(exception, message); - if (exception is PlatformHttpException) + if (exception is PlatformHttpException phe) { - PlatformHttpException phe = exception as PlatformHttpException; return StatusCode((int)phe.Response.StatusCode, phe.Message); } - else if (exception is ServiceException) + else if (exception is ServiceException se) { - ServiceException se = exception as ServiceException; return StatusCode((int)se.StatusCode, se.Message); } return StatusCode(500, $"{message}"); } - private async Task CreateBinaryData(Instance instanceBefore, string dataType, string contentType, string filename, Stream fileStream) + private async Task CreateBinaryData(Instance instanceBefore, string dataType, string contentType, string? filename, Stream fileStream) { int instanceOwnerPartyId = int.Parse(instanceBefore.Id.Split("/")[0]); Guid instanceGuid = Guid.Parse(instanceBefore.Id.Split("/")[1]); @@ -459,7 +455,7 @@ private async Task CreateAppModelData( { Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - object appModel; + object? appModel; string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); @@ -472,7 +468,7 @@ private async Task CreateAppModelData( ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error)) + if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) { return BadRequest(deserializer.Error); } @@ -510,7 +506,7 @@ private async Task GetBinaryData( if (dataStream != null) { - string userOrgClaim = User.GetOrg(); + string? userOrgClaim = User.GetOrg(); if (userOrgClaim == null || !org.Equals(userOrgClaim, StringComparison.InvariantCultureIgnoreCase)) { await _instanceClient.UpdateReadStatus(instanceOwnerPartyId, instanceGuid, "read"); @@ -588,7 +584,7 @@ private async Task GetFormData( await _dataProcessor.ProcessDataRead(instance, dataGuid, appModel); - string userOrgClaim = User.GetOrg(); + string? userOrgClaim = User.GetOrg(); if (userOrgClaim == null || !org.Equals(userOrgClaim, StringComparison.InvariantCultureIgnoreCase)) { await _instanceClient.UpdateReadStatus(instanceOwnerId, instanceGuid, "read"); @@ -602,7 +598,7 @@ private async Task PutBinaryData(int instanceOwnerPartyId, Guid in if (Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) { var contentDispositionHeader = ContentDispositionHeaderValue.Parse(headerValues.ToString()); - _logger.LogInformation("Content-Disposition: {ContentDisposition}", headerValues); + _logger.LogInformation("Content-Disposition: {ContentDisposition}", headerValues.ToString()); DataElement dataElement = await _dataClient.UpdateBinaryData(new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), Request.ContentType, contentDispositionHeader.FileName.ToString(), dataGuid, Request.Body); SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request); @@ -620,7 +616,7 @@ private async Task PutFormData(string org, string app, Instance in Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - object serviceModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + object? serviceModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); if (!string.IsNullOrEmpty(deserializer.Error)) { @@ -632,7 +628,7 @@ private async Task PutFormData(string org, string app, Instance in return BadRequest("No data found in content"); } - Dictionary changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, serviceModel, _dataProcessor, _logger); + Dictionary? changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, serviceModel, _dataProcessor, _logger); await UpdatePresentationTextsOnInstance(instance, dataType, serviceModel); await UpdateDataValuesOnInstance(instance, dataType, serviceModel); @@ -652,8 +648,10 @@ private async Task PutFormData(string org, string app, Instance in string dataUrl = updatedDataElement.SelfLinks.Apps; if (changedFields is not null) { - CalculationResult calculationResult = new CalculationResult(updatedDataElement); - calculationResult.ChangedFields = changedFields; + CalculationResult calculationResult = new(updatedDataElement) + { + ChangedFields = changedFields + }; return StatusCode((int)HttpStatusCode.SeeOther, calculationResult); } diff --git a/src/Altinn.App.Api/Controllers/DataTagsController.cs b/src/Altinn.App.Api/Controllers/DataTagsController.cs index 6526da82a..8ffda6c36 100644 --- a/src/Altinn.App.Api/Controllers/DataTagsController.cs +++ b/src/Altinn.App.Api/Controllers/DataTagsController.cs @@ -1,3 +1,4 @@ +#nullable enable using System.Net.Mime; using System.Text.RegularExpressions; @@ -65,7 +66,7 @@ public async Task> Get( return NotFound($"Unable to find instance based on the given parameters."); } - DataElement dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { @@ -112,7 +113,7 @@ public async Task> Add( return NotFound("Unable to find instance based on the given parameters."); } - DataElement dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { @@ -161,7 +162,7 @@ public async Task Delete( return NotFound("Unable to find instance based on the given parameters."); } - DataElement dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index 70102d018..38a75af7e 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -218,14 +218,14 @@ public async Task> Post( // create minimum instance template instanceTemplate = new Instance { - InstanceOwner = new InstanceOwner { PartyId = instanceOwnerPartyId.Value.ToString() } + InstanceOwner = new InstanceOwner { PartyId = instanceOwnerPartyId!.Value.ToString() } }; } ApplicationMetadata application = await _appMetadata.GetApplicationMetadata(); RequestPartValidator requestValidator = new RequestPartValidator(application); - string multipartError = requestValidator.ValidateParts(parsedRequest.Parts); + string? multipartError = requestValidator.ValidateParts(parsedRequest.Parts); if (!string.IsNullOrEmpty(multipartError)) { @@ -469,7 +469,7 @@ public async Task> PostSimplified( instance = await _instanceClient.CreateInstance(org, app, instanceTemplate); - if (copySourceInstance) + if (copySourceInstance && source is not null) { await CopyDataFromSourceInstance(application, instance, source); } @@ -958,7 +958,7 @@ private async Task StorePrefillParts(Instance instance, ApplicationMetadata appI DataElement dataElement; if (dataType?.AppLogic?.ClassRef != null) { - _logger.LogInformation($"Storing part {part.Name}"); + _logger.LogInformation("Storing part {partName}", part.Name); Type type; try @@ -973,12 +973,12 @@ private async Task StorePrefillParts(Instance instance, ApplicationMetadata appI ModelDeserializer deserializer = new ModelDeserializer(_logger, type); object? data = await deserializer.DeserializeAsync(part.Stream, part.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error)) + if (!string.IsNullOrEmpty(deserializer.Error) || data is null) { throw new InvalidOperationException(deserializer.Error); } - await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, part.Name, data); + await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, part.Name!, data); await _instantiationProcessor.DataCreation(instance, data, null); @@ -989,11 +989,11 @@ private async Task StorePrefillParts(Instance instance, ApplicationMetadata appI org, app, instanceOwnerIdAsInt, - part.Name); + part.Name!); } else { - dataElement = await _dataClient.InsertBinaryData(instance.Id, part.Name, part.ContentType, part.FileName, part.Stream); + dataElement = await _dataClient.InsertBinaryData(instance.Id, part.Name!, part.ContentType, part.FileName, part.Stream); } if (dataElement == null) diff --git a/src/Altinn.App.Api/Controllers/OptionsController.cs b/src/Altinn.App.Api/Controllers/OptionsController.cs index 636b9777b..1b64ca056 100644 --- a/src/Altinn.App.Api/Controllers/OptionsController.cs +++ b/src/Altinn.App.Api/Controllers/OptionsController.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -38,10 +40,10 @@ public OptionsController(IAppOptionsService appOptionsService) [HttpGet("{optionsId}")] public async Task Get( [FromRoute] string optionsId, - [FromQuery] string language, + [FromQuery] string? language, [FromQuery] Dictionary queryParams) { - AppOptions appOptions = await _appOptionsService.GetOptionsAsync(optionsId, language, queryParams); + AppOptions appOptions = await _appOptionsService.GetOptionsAsync(optionsId, language ?? "nb", queryParams); if (appOptions.Options == null) { return NotFound(); @@ -74,12 +76,12 @@ public async Task Get( [FromRoute] int instanceOwnerPartyId, [FromRoute] Guid instanceGuid, [FromRoute] string optionsId, - [FromQuery] string language, + [FromQuery] string? language, [FromQuery] Dictionary queryParams) { var instanceIdentifier = new InstanceIdentifier(instanceOwnerPartyId, instanceGuid); - AppOptions appOptions = await _appOptionsService.GetOptionsAsync(instanceIdentifier, optionsId, language, queryParams); + AppOptions appOptions = await _appOptionsService.GetOptionsAsync(instanceIdentifier, optionsId, language ?? "nb", queryParams); // Only return NotFound if we can't find an options provider. // If we find the options provider, but it doesnt' have values, return empty list. diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index f3e29404e..f3bbe87fe 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -257,7 +257,7 @@ public async Task> NextElement( Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - if (instance.Process == null) + if (instance?.Process == null) { return Conflict($"Process is not started. Use start!"); } @@ -267,7 +267,7 @@ public async Task> NextElement( return Conflict($"Process is ended."); } - string? altinnTaskType = instance?.Process?.CurrentTask?.AltinnTaskType; + string? altinnTaskType = instance.Process?.CurrentTask?.AltinnTaskType; if (altinnTaskType == null) { @@ -276,7 +276,7 @@ public async Task> NextElement( bool authorized; string? checkedAction = EnsureActionNotTaskType(processNext?.Action ?? altinnTaskType); - authorized = await AuthorizeAction(checkedAction, org, app, instanceOwnerPartyId, instanceGuid, instance?.Process?.CurrentTask?.ElementId); + authorized = await AuthorizeAction(checkedAction, org, app, instanceOwnerPartyId, instanceGuid, instance.Process?.CurrentTask?.ElementId); if (!authorized) { @@ -370,9 +370,9 @@ public async Task> CompleteProcess( int counter = 0; do { - string? altinnTaskType = EnsureActionNotTaskType(instance?.Process?.CurrentTask?.AltinnTaskType); + string altinnTaskType = EnsureActionNotTaskType(instance.Process.CurrentTask.AltinnTaskType); - bool authorized = await AuthorizeAction(altinnTaskType, org, app, instanceOwnerPartyId, instanceGuid, instance?.Process?.CurrentTask?.ElementId); + bool authorized = await AuthorizeAction(altinnTaskType, org, app, instanceOwnerPartyId, instanceGuid, instance.Process.CurrentTask.ElementId); if (!authorized) { return Forbid(); @@ -404,7 +404,12 @@ public async Task> CompleteProcess( } } - currentTaskId = result.ProcessStateChange?.NewProcessState.CurrentTask.ElementId; + if (result.ProcessStateChange?.NewProcessState is null) + { + return StatusCode(500, "Something is not right"); + } + + currentTaskId = result.ProcessStateChange.NewProcessState.CurrentTask.ElementId; } catch (Exception ex) { @@ -413,11 +418,11 @@ public async Task> CompleteProcess( counter++; } - while (instance?.Process?.EndEvent == null || counter > MaxIterationsAllowed); + while (instance.Process.EndEvent == null || counter > MaxIterationsAllowed); if (counter > MaxIterationsAllowed) { - _logger.LogError($"More than {counter} iterations detected in process. Possible loop. Fix app {org}/{app}'s process definition!"); + _logger.LogError($"More than {MaxIterationsAllowed} iterations detected in process. Possible loop. Fix app's process definition!"); return StatusCode(500, $"More than {counter} iterations detected in process. Possible loop. Fix app process definition!"); } @@ -477,14 +482,12 @@ private ActionResult ExceptionResponse(Exception exception, string message) { _logger.LogError(exception, message); - if (exception is PlatformHttpException) + if (exception is PlatformHttpException phe) { - PlatformHttpException phe = exception as PlatformHttpException; return StatusCode((int)phe.Response.StatusCode, phe.Message); } - else if (exception is ServiceException) + else if (exception is ServiceException se) { - ServiceException se = exception as ServiceException; return StatusCode((int)se.StatusCode, se.Message); } @@ -496,7 +499,7 @@ private async Task AuthorizeAction(string action, string org, string app, return await _authorization.AuthorizeAction(new AppIdentifier(org, app), new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), HttpContext.User, action, taskId); } - private static string? EnsureActionNotTaskType(string? actionOrTaskType) + private static string EnsureActionNotTaskType(string actionOrTaskType) { switch (actionOrTaskType) { diff --git a/src/Altinn.App.Api/Controllers/ProfileController.cs b/src/Altinn.App.Api/Controllers/ProfileController.cs index 7fd381c59..c5d01cc29 100644 --- a/src/Altinn.App.Api/Controllers/ProfileController.cs +++ b/src/Altinn.App.Api/Controllers/ProfileController.cs @@ -1,3 +1,5 @@ +#nullable enable + using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Profile; using Microsoft.AspNetCore.Authorization; @@ -14,16 +16,14 @@ namespace Altinn.App.Api.Controllers public class ProfileController : Controller { private readonly IProfileClient _profileClient; - private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; /// /// Initializes a new instance of the class /// - public ProfileController(IProfileClient profileClient, IHttpContextAccessor httpContextAccessor, ILogger logger) + public ProfileController(IProfileClient profileClient, ILogger logger) { _profileClient = profileClient; - _httpContextAccessor = httpContextAccessor; _logger = logger; } @@ -35,7 +35,7 @@ public ProfileController(IProfileClient profileClient, IHttpContextAccessor http [HttpGet("user")] public async Task GetUser() { - int userId = AuthenticationHelper.GetUserId(_httpContextAccessor.HttpContext); + int userId = AuthenticationHelper.GetUserId(HttpContext); if (userId == 0) { return BadRequest("The userId is not proviced in the context."); diff --git a/src/Altinn.App.Api/Controllers/StatelessDataController.cs b/src/Altinn.App.Api/Controllers/StatelessDataController.cs index 4c40200f5..a222724d1 100644 --- a/src/Altinn.App.Api/Controllers/StatelessDataController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessDataController.cs @@ -205,7 +205,7 @@ public async Task Post( ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error)) + if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) { return BadRequest(deserializer.Error); } @@ -248,7 +248,7 @@ public async Task PostAnonymous([FromQuery] string dataType) ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error)) + if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) { return BadRequest(deserializer.Error); } diff --git a/src/Altinn.App.Api/Controllers/TextsController.cs b/src/Altinn.App.Api/Controllers/TextsController.cs index eae00f1d6..818a9e118 100644 --- a/src/Altinn.App.Api/Controllers/TextsController.cs +++ b/src/Altinn.App.Api/Controllers/TextsController.cs @@ -1,3 +1,5 @@ +#nullable enable + using Altinn.App.Core.Internal.App; using Altinn.Platform.Storage.Interface.Models; @@ -37,7 +39,7 @@ public async Task> Get(string org, string app, [FromR return BadRequest($"Provided language {language} is invalid. Language code should consists of two characters."); } - TextResource textResource = await _appResources.GetTexts(org, app, language); + TextResource? textResource = await _appResources.GetTexts(org, app, language); if (textResource == null && language != "nb") { diff --git a/src/Altinn.App.Api/Controllers/ValidateController.cs b/src/Altinn.App.Api/Controllers/ValidateController.cs index 4d67fc1ba..3c376af73 100644 --- a/src/Altinn.App.Api/Controllers/ValidateController.cs +++ b/src/Altinn.App.Api/Controllers/ValidateController.cs @@ -1,3 +1,5 @@ +#nullable enable + using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients; @@ -50,13 +52,13 @@ public async Task ValidateInstance( [FromRoute] int instanceOwnerPartyId, [FromRoute] Guid instanceGuid) { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + Instance? instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); if (instance == null) { return NotFound(); } - string taskId = instance.Process?.CurrentTask?.ElementId; + string? taskId = instance.Process?.CurrentTask?.ElementId; if (taskId == null) { throw new ValidationException("Unable to validate instance without a started process."); @@ -96,7 +98,7 @@ public async Task ValidateData( [FromRoute] Guid instanceId, [FromRoute] Guid dataGuid) { - Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerId, instanceId); + Instance? instance = await _instanceClient.GetInstance(app, org, instanceOwnerId, instanceId); if (instance == null) { return NotFound(); @@ -112,7 +114,7 @@ public async Task ValidateData( List messages = new List(); - DataElement element = instance.Data.FirstOrDefault(d => d.Id == dataGuid.ToString()); + DataElement? element = instance.Data.FirstOrDefault(d => d.Id == dataGuid.ToString()); if (element == null) { @@ -121,7 +123,7 @@ public async Task ValidateData( Application application = await _appMetadata.GetApplicationMetadata(); - DataType dataType = application.DataTypes.FirstOrDefault(et => et.Id == element.DataType); + DataType? dataType = application.DataTypes.FirstOrDefault(et => et.Id == element.DataType); if (dataType == null) { diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs b/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs index ff1d8fddd..63c7d29d2 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/MultipartRequestReader.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; +#nullable enable + using Altinn.App.Core.Helpers.Extensions; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Net.Http.Headers; @@ -62,15 +59,13 @@ public async Task Read() { MultipartReader reader = new MultipartReader(GetBoundary(), request.Body); - MultipartSection section; + MultipartSection? section; while ((section = await reader.ReadNextSectionAsync()) != null) { partCounter++; - bool hasContentDispositionHeader = ContentDispositionHeaderValue - .TryParse(section.ContentDisposition, out ContentDispositionHeaderValue contentDisposition); - - if (!hasContentDispositionHeader) + if (!ContentDispositionHeaderValue + .TryParse(section.ContentDisposition, out ContentDispositionHeaderValue? contentDisposition)) { Errors.Add(string.Format("Part number {0} doesn't have a content disposition", partCounter)); continue; @@ -82,8 +77,8 @@ public async Task Read() continue; } - string sectionName = contentDisposition.Name.HasValue ? contentDisposition.Name.Value : null; - string contentFileName = null; + string? sectionName = contentDisposition.Name.Value; + string? contentFileName = null; if (contentDisposition.FileNameStar.HasValue) { contentFileName = contentDisposition.FileNameStar.Value; @@ -135,7 +130,7 @@ public async Task Read() private string GetBoundary() { MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(request.ContentType); - return mediaType.Boundary.Value.Trim('"'); + return mediaType.Boundary.Value!.Trim('"'); } } } diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs index ea5faf378..1f92dd925 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPart.cs @@ -1,4 +1,4 @@ -using System.IO; +#nullable enable namespace Altinn.App.Api.Helpers.RequestHandling { @@ -10,25 +10,25 @@ public class RequestPart /// /// The stream to access this part. /// - public Stream Stream { get; set; } + public Stream Stream { get; set; } = default!; /// /// The file name as given in content description. /// - public string FileName { get; set; } + public string? FileName { get; set; } /// /// The parts name. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// The content type of the part. /// - public string ContentType { get; set; } + public string ContentType { get; set; } = default!; /// - /// The file size of the part, if given. + /// The file size of the part, 0 if not given. /// public long FileSize { get; set; } } diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs index 9ba71fc67..2142a9495 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Linq; using Altinn.Platform.Storage.Interface.Models; @@ -26,7 +28,7 @@ public RequestPartValidator(Application appInfo) /// /// The request part to be validated. /// null if no errors where found. Otherwise an error message. - public string ValidatePart(RequestPart part) + public string? ValidatePart(RequestPart part) { if (part.Name == "instance") { @@ -44,7 +46,7 @@ public string ValidatePart(RequestPart part) Console.WriteLine($"// {DateTime.Now} // Debug // appinfo : {appInfo}"); Console.WriteLine($"// {DateTime.Now} // Debug // appinfo.Id : {appInfo.Id}"); - DataType dataType = appInfo.DataTypes.Find(e => e.Id == part.Name); + DataType? dataType = appInfo.DataTypes.Find(e => e.Id == part.Name); Console.WriteLine($"// {DateTime.Now} // Debug // elementType : {dataType}"); @@ -91,11 +93,11 @@ public string ValidatePart(RequestPart part) /// /// The list of request parts to be validated. /// null if no errors where found. Otherwise an error message. - public string ValidateParts(List parts) + public string? ValidateParts(List parts) { foreach (RequestPart part in parts) { - string partError = ValidatePart(part); + string? partError = ValidatePart(part); if (partError != null) { return partError; diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs index 83240cb4e..7ee5921ac 100644 --- a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs +++ b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs @@ -41,13 +41,13 @@ public ModelDeserializer(ILogger logger, Type modelType) /// The data stream to deserialize. /// The content type of the stream. /// An instance of the initialized type if deserializing succeed. - public async Task DeserializeAsync(Stream stream, string contentType) + public async Task DeserializeAsync(Stream stream, string? contentType) { Error = null; if (contentType == null) { - Error = $"Unknown content type {contentType}. Cannot read the data."; + Error = $"Unknown content type \"null\". Cannot read the data."; return null; } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs index 65170b2f7..7d5b5d0ac 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/KeyVault/SecretsLocalClient.cs @@ -3,7 +3,6 @@ using Microsoft.Azure.KeyVault; using Microsoft.Azure.KeyVault.WebKey; using Microsoft.Extensions.Configuration; -using Newtonsoft.Json.Linq; namespace Altinn.App.Core.Infrastructure.Clients.KeyVault { @@ -24,29 +23,19 @@ public SecretsLocalClient(IConfiguration configuration) } /// - public async Task GetCertificateAsync(string certificateId) + public Task GetCertificateAsync(string certificateName) { - string token = GetTokenFromSecrets(certificateId); - if (!string.IsNullOrEmpty(token)) - { - byte[] localCertBytes = Convert.FromBase64String(token); - return await Task.FromResult(localCertBytes); - } - - return null; + string token = GetTokenFromSecrets(certificateName); + byte[] localCertBytes = Convert.FromBase64String(token); + return Task.FromResult(localCertBytes); } /// - public async Task GetKeyAsync(string keyId) + public Task GetKeyAsync(string keyName) { - string token = GetTokenFromSecrets(keyId); - if (!string.IsNullOrEmpty(token)) - { - JsonWebKey key = JsonSerializer.Deserialize(token); - return await Task.FromResult(key); - } - - return null; + string token = GetTokenFromSecrets(keyName); + JsonWebKey key = JsonSerializer.Deserialize(token)!; + return Task.FromResult(key); } /// @@ -62,25 +51,28 @@ public async Task GetSecretAsync(string secretId) return await Task.FromResult(token); } - private string GetTokenFromSecrets(string tokenId) - => GetTokenFromConfiguration(tokenId) ?? - GetTokenFromLocalSecrets(tokenId); + private string GetTokenFromSecrets(string secretId) + => GetTokenFromLocalSecrets(secretId) ?? + GetTokenFromConfiguration(secretId) ?? + throw new ArgumentException($"SecretId={secretId} does not exist in appsettings or secrets.json"); - private string GetTokenFromConfiguration(string tokenId) + private string? GetTokenFromConfiguration(string tokenId) => _configuration[tokenId]; - private static string GetTokenFromLocalSecrets(string tokenId) + private static string? GetTokenFromLocalSecrets(string secretId) { string path = Path.Combine(Directory.GetCurrentDirectory(), @"secrets.json"); if (File.Exists(path)) { string jsonString = File.ReadAllText(path); - JObject keyVault = JObject.Parse(jsonString); - keyVault.TryGetValue(tokenId, out JToken? token); - return token != null ? token.ToString() : string.Empty; + var document = JsonDocument.Parse(jsonString, new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }); + if (document.RootElement.TryGetProperty(secretId, out var jsonElement)) + { + return jsonElement.GetString(); + } } - return string.Empty; + return null; } } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index c9fcc48e6..14bf9e4f0 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -293,7 +293,7 @@ public async Task InsertBinaryData(string org, string app, int inst } /// - public async Task InsertBinaryData(string instanceId, string dataType, string contentType, string filename, Stream stream, string? generatedFromTask = null) + public async Task InsertBinaryData(string instanceId, string dataType, string contentType, string? filename, Stream stream, string? generatedFromTask = null) { string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceId}/data?dataType={dataType}"; if(!string.IsNullOrEmpty(generatedFromTask)) diff --git a/src/Altinn.App.Core/Internal/Data/IDataClient.cs b/src/Altinn.App.Core/Internal/Data/IDataClient.cs index 0a727f3fc..61f361e19 100644 --- a/src/Altinn.App.Core/Internal/Data/IDataClient.cs +++ b/src/Altinn.App.Core/Internal/Data/IDataClient.cs @@ -142,7 +142,7 @@ public interface IDataClient /// the stream to stream /// Optional field to set what task the binary data was generated from /// - Task InsertBinaryData(string instanceId, string dataType, string contentType, string filename, Stream stream, string? generatedFromTask = null); + Task InsertBinaryData(string instanceId, string dataType, string contentType, string? filename, Stream stream, string? generatedFromTask = null); /// /// Updates the data element metadata object. diff --git a/src/Altinn.App.Core/Models/CalculationResult.cs b/src/Altinn.App.Core/Models/CalculationResult.cs index 66eda5053..e3e644980 100644 --- a/src/Altinn.App.Core/Models/CalculationResult.cs +++ b/src/Altinn.App.Core/Models/CalculationResult.cs @@ -1,3 +1,5 @@ +#nullable enable + using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Models @@ -28,7 +30,7 @@ public CalculationResult(DataElement dataElement) /// /// The DataElement base object /// The changed fields - public CalculationResult(DataElement dataElement, Dictionary changedFields) + public CalculationResult(DataElement dataElement, Dictionary changedFields) { MapDataElementToCalculationResult(dataElement); ChangedFields = changedFields; @@ -37,7 +39,7 @@ public CalculationResult(DataElement dataElement, Dictionary cha /// /// The key-value pair of fields changed by a calculation /// - public Dictionary ChangedFields { get; set; } + public Dictionary? ChangedFields { get; set; } private void MapDataElementToCalculationResult(DataElement dataElement) { diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 8bf4bff1d..c8d35e391 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -7,7 +7,7 @@ $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) - preview + preview.0 v true 10.0 diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj index b5be26e26..ccfd2ce52 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -5,6 +5,11 @@ enable enable false + $(NoWarn);CS1591;CS0618 + diff --git a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs index cb73343c7..c888ab9f8 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs @@ -9,6 +9,7 @@ using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; namespace Altinn.App.Api.Tests.Controllers { @@ -18,6 +19,29 @@ public DataControllerTests(WebApplicationFactory factory) : base(factor { } + [Fact] + public async Task PutDataElement_MissingDataType_ReturnsBadRequest() + { + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 1337; + Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + TestData.DeleteInstance(org, app, instanceOwnerPartyId, guid); + TestData.PrepareInstance(org, app, instanceOwnerPartyId, guid); + + + using var content = new StringContent("{}", System.Text.Encoding.UTF8, "application/json"); // empty valid json + var response = await client.PostAsync($"/{org}/{app}/instances/{instanceOwnerPartyId}/{guid}/data", content); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + var responseContent = await response.Content.ReadAsStringAsync(); + responseContent.Should().Contain("dataType"); + } + [Fact] public async Task CreateDataElement_BinaryPdf_AnalyserShouldRunOk() { diff --git a/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs index 259d82673..2bf455d4a 100644 --- a/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs @@ -26,8 +26,32 @@ public async Task Get_ShouldReturnParametersInHeader() string app = "contributer-restriction"; HttpClient client = GetRootedClient(org, app); + string url = $"/{org}/{app}/api/options/test?language=esperanto"; + HttpResponseMessage response = await client.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + response.StatusCode.Should().Be(HttpStatusCode.OK, content); + + var headerValue = response.Headers.GetValues("Altinn-DownstreamParameters"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + headerValue.Should().Contain("lang=esperanto"); + } + + [Fact] + public async Task Get_ShouldDefaultToNbLanguage() + { + OverrideServicesForThisTest = (services) => + { + services.AddTransient(); + }; + + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + string url = $"/{org}/{app}/api/options/test"; HttpResponseMessage response = await client.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + response.StatusCode.Should().Be(HttpStatusCode.OK, content); var headerValue = response.Headers.GetValues("Altinn-DownstreamParameters"); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -45,8 +69,9 @@ public Task GetAppOptionsAsync(string language, Dictionary() { - { "lang", "nb" } - } + { "lang", language } + }, + }; return Task.FromResult(appOptions); diff --git a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj index b40354e5c..ca11a7812 100644 --- a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj +++ b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj @@ -5,6 +5,12 @@ enable false + + $(NoWarn);CS1591;CS0618 + diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index f3156bb10..90a88f825 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -7,6 +7,13 @@ Altinn.App.Core.Tests enable + true + + $(NoWarn);CS1591;CS0618 + @@ -30,6 +37,7 @@ Always + @@ -101,9 +109,5 @@ ..\..\Altinn3.ruleset - - true - $(NoWarn);1591 - diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/KeyVault/SecretsLocalClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/KeyVault/SecretsLocalClientTests.cs new file mode 100644 index 000000000..27351b949 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/KeyVault/SecretsLocalClientTests.cs @@ -0,0 +1,63 @@ +namespace Altinn.App.Core.Tests.Infrastructure.Clients.KeyVault; + +using System.Text.Json; +using Altinn.App.Core.Infrastructure.Clients.KeyVault; + +using FluentAssertions; +using Microsoft.Azure.KeyVault.WebKey; +using Microsoft.Extensions.Configuration; +using Xunit; + +public class SecretsLocalClientTests +{ + public static IConfiguration GetConfiguration(params (string Key, string Value)[] keys) + => new ConfigurationBuilder() + .AddInMemoryCollection(keys.ToDictionary(k => k.Key, k => k.Value)) + .Build(); + + [Fact] + public async Task TestMissingSecretId_ThrowsException() + { + var sut = new SecretsLocalClient(GetConfiguration(("test", "value"), ("d", "e"))); + + await sut.Invoking(s => s.GetCertificateAsync("certId")).Should().ThrowAsync(); + await sut.Invoking(s => s.GetKeyAsync("certId")).Should().ThrowAsync(); + await sut.Invoking(s => s.GetSecretAsync("certId")).Should().ThrowAsync(); + } + + [Fact] + public async Task TestCertificateFoundInConfiguration() + { + var certificate = new byte[20]; + Random.Shared.NextBytes(certificate); // Initialize with a randmo value + + var sut = new SecretsLocalClient(GetConfiguration(("certId", Convert.ToBase64String(certificate)), ("d", "e"))); + + var certResult = await sut.GetCertificateAsync("certId"); + certResult.Should().BeEquivalentTo(certificate); + } + + [Fact] + public async Task TestSecretFoundInSecretsJson() + { + var sut = new SecretsLocalClient(GetConfiguration()); + + var secretResult = await sut.GetSecretAsync("secretId"); + secretResult.Should().Be("local secret dummy data"); + } + + [Fact] + public async Task TestKeyFoundInSecretsJson() + { + var jwk = new JsonWebKey() + { + CurveName = "sillyCurveForTest" + }; + var jwkSerialized = JsonSerializer.Serialize(jwk); + var sut = new SecretsLocalClient(GetConfiguration(("jwk", jwkSerialized))); + + var keyResult = await sut.GetKeyAsync("jwk"); + keyResult.Should().BeEquivalentTo(jwk); + keyResult.CurveName.Should().Be("sillyCurveForTest"); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/secrets.json b/test/Altinn.App.Core.Tests/secrets.json new file mode 100644 index 000000000..7e98541de --- /dev/null +++ b/test/Altinn.App.Core.Tests/secrets.json @@ -0,0 +1,3 @@ +{ + "secretId": "local secret dummy data" +} \ No newline at end of file From 58862ae142fca1d7ba955090c998601a8c3b0936 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Mon, 13 Nov 2023 15:25:54 +0100 Subject: [PATCH 22/46] New field AltinnNugetVersion in applicationMetadata.cs (#333) --- src/Altinn.App.Core/Models/ApplicationMetadata.cs | 9 ++++++++- test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs | 4 +++- ...unmapped-properties.applicationmetadata.expected.json | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Altinn.App.Core/Models/ApplicationMetadata.cs b/src/Altinn.App.Core/Models/ApplicationMetadata.cs index 24beaf62e..db28abbf0 100644 --- a/src/Altinn.App.Core/Models/ApplicationMetadata.cs +++ b/src/Altinn.App.Core/Models/ApplicationMetadata.cs @@ -1,3 +1,4 @@ +using System.Reflection; using Altinn.Platform.Storage.Interface.Models; using Newtonsoft.Json; @@ -57,7 +58,13 @@ public ApplicationMetadata(string id) /// [JsonProperty(PropertyName = "logo")] public Logo? Logo { get; set; } - + + /// + /// Frontend sometimes need to have knowledge of the nuget package version for backwards compatibility + /// + [JsonProperty(PropertyName = "altinnNugetVersion")] + public string AltinnNugetVersion { get; set; } = typeof(ApplicationMetadata).Assembly!.GetName().Version!.ToString(); + /// /// Holds properties that are not mapped to other properties /// diff --git a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs index aefa1244b..84675dd8f 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/AppMedataTest.cs @@ -1,3 +1,4 @@ +using System.Reflection; using System.Text.Json; using Altinn.App.Core.Configuration; using Altinn.App.Core.Internal.App; @@ -482,8 +483,9 @@ public async Task GetApplicationMetadata_deserialize_serialize_unmapped_properti AppSettings appSettings = GetAppSettings("AppMetadata", "unmapped-properties.applicationmetadata.json"); IAppMetadata appMetadata = SetupAppMedata(Options.Create(appSettings)); var appMetadataObj = await appMetadata.GetApplicationMetadata(); - string serialized = JsonSerializer.Serialize(appMetadataObj, new JsonSerializerOptions { WriteIndented = true }); + string serialized = JsonSerializer.Serialize(appMetadataObj, new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); string expected = File.ReadAllText(Path.Join(appBasePath, "AppMetadata", "unmapped-properties.applicationmetadata.expected.json")); + expected = expected.Replace("--AltinnNugetVersion--", typeof(ApplicationMetadata).Assembly!.GetName().Version!.ToString()); serialized.Should().Be(expected); } diff --git a/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/unmapped-properties.applicationmetadata.expected.json b/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/unmapped-properties.applicationmetadata.expected.json index de16c54b3..88b4be5b3 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/unmapped-properties.applicationmetadata.expected.json +++ b/test/Altinn.App.Core.Tests/Internal/App/TestData/AppMetadata/unmapped-properties.applicationmetadata.expected.json @@ -10,6 +10,7 @@ "Show": "select-instance" }, "Logo": null, + "AltinnNugetVersion": "--AltinnNugetVersion--", "VersionId": null, "Org": "tdd", "Title": { From c141a3f8a2a8fa27929420a6bbda8501b166fd43 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Tue, 14 Nov 2023 09:32:27 +0100 Subject: [PATCH 23/46] Fix more warnings (#331) --- src/Altinn.App.Api/Controllers/PagesController.cs | 4 ++-- src/Altinn.App.Api/Helpers/StartupHelper.cs | 2 +- .../Controllers/ValidateControllerValidateDataTests.cs | 3 ++- .../Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs | 2 +- .../Mocks/PepWithPDPAuthorizationMockSI.cs | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Altinn.App.Api/Controllers/PagesController.cs b/src/Altinn.App.Api/Controllers/PagesController.cs index c9181ad4e..dc212350a 100644 --- a/src/Altinn.App.Api/Controllers/PagesController.cs +++ b/src/Altinn.App.Api/Controllers/PagesController.cs @@ -31,8 +31,8 @@ public class PagesController : ControllerBase /// The page order service public PagesController( IAppModel appModel, - IAppResources resources, - IPageOrder pageOrder, + IAppResources resources, + IPageOrder pageOrder, ILogger logger) { _appModel = appModel; diff --git a/src/Altinn.App.Api/Helpers/StartupHelper.cs b/src/Altinn.App.Api/Helpers/StartupHelper.cs index 9bac3be5f..6ef8a00ea 100644 --- a/src/Altinn.App.Api/Helpers/StartupHelper.cs +++ b/src/Altinn.App.Api/Helpers/StartupHelper.cs @@ -43,6 +43,6 @@ public static string GetApplicationId() { string appMetaDataString = File.ReadAllText("config/applicationmetadata.json"); JObject appMetadataJObject = JObject.Parse(appMetaDataString); - return appMetadataJObject.SelectToken("id").Value(); + return appMetadataJObject.SelectToken("id")?.Value() ?? throw new Exception("config/applicationmetadata.json does not contain an \"id\" property"); } } \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs index 3cb418a19..2e76cc67c 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs @@ -9,6 +9,7 @@ using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Mvc; +using FluentAssertions; using Moq; using Xunit; @@ -221,7 +222,7 @@ public async Task TestValidateData(ValidateDataTestScenario testScenario) { var result = await validateController.ValidateData(org, app, instanceOwnerId, testScenario.InstanceId, testScenario.DataGuid); - Assert.IsType(testScenario.ExpectedResult, result); + result.Should().BeOfType(testScenario.ExpectedResult); } else { diff --git a/test/Altinn.App.Api.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs b/test/Altinn.App.Api.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs index 6ff3aae21..2e3a9f2ce 100644 --- a/test/Altinn.App.Api.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs +++ b/test/Altinn.App.Api.Tests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs @@ -14,7 +14,7 @@ namespace Altinn.App.Api.Tests.Mocks.Authentication public class JwtCookiePostConfigureOptionsStub : IPostConfigureOptions { /// - public void PostConfigure(string name, JwtCookieOptions options) + public void PostConfigure(string? name, JwtCookieOptions options) { if (string.IsNullOrEmpty(options.JwtCookieName)) { diff --git a/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs b/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs index a4ad04ad2..bb0100c59 100644 --- a/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs +++ b/test/Altinn.App.Api.Tests/Mocks/PepWithPDPAuthorizationMockSI.cs @@ -61,7 +61,7 @@ public async Task GetDecisionForRequest(XacmlJsonRequestRoot } catch { - return null; + return null!; } } From 98f861397b89f664d18b865be2fdf901da1b4c86 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Wed, 15 Nov 2023 12:15:42 +0100 Subject: [PATCH 24/46] Add [Authorize] attribute on AuthorizationController.GetCurrentParty (#338) --- src/Altinn.App.Api/Controllers/AuthorizationController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Altinn.App.Api/Controllers/AuthorizationController.cs b/src/Altinn.App.Api/Controllers/AuthorizationController.cs index d3a5f2eb5..97530f181 100644 --- a/src/Altinn.App.Api/Controllers/AuthorizationController.cs +++ b/src/Altinn.App.Api/Controllers/AuthorizationController.cs @@ -39,6 +39,7 @@ public AuthorizationController( /// Gets current party by reading cookie value and validating. /// /// Party id for selected party. If invalid, partyId for logged in user is returned. + [Authorize] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [HttpGet("{org}/{app}/api/authorization/parties/current")] public async Task GetCurrentParty(bool returnPartyObject = false) From d7b8c5e44f068d5b0661ec0db91316eb71db9291 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Wed, 15 Nov 2023 16:11:53 +0100 Subject: [PATCH 25/46] Mark IPageOrder [Obsolete] as it will be unused in frontend V4 (#343) --- src/Altinn.App.Api/Controllers/PagesController.cs | 1 + src/Altinn.App.Api/Controllers/StatelessPagesController.cs | 1 + src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs | 2 ++ src/Altinn.App.Core/Features/IPageOrder.cs | 1 + src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs | 1 + 5 files changed, 6 insertions(+) diff --git a/src/Altinn.App.Api/Controllers/PagesController.cs b/src/Altinn.App.Api/Controllers/PagesController.cs index dc212350a..2eff128fe 100644 --- a/src/Altinn.App.Api/Controllers/PagesController.cs +++ b/src/Altinn.App.Api/Controllers/PagesController.cs @@ -15,6 +15,7 @@ namespace Altinn.App.Api.Controllers [Authorize] [ApiController] [Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/pages")] + [Obsolete("IPageOrder does not work with frontend version 4")] public class PagesController : ControllerBase { private readonly IAppModel _appModel; diff --git a/src/Altinn.App.Api/Controllers/StatelessPagesController.cs b/src/Altinn.App.Api/Controllers/StatelessPagesController.cs index eb9abc586..f291cbcbb 100644 --- a/src/Altinn.App.Api/Controllers/StatelessPagesController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessPagesController.cs @@ -15,6 +15,7 @@ namespace Altinn.App.Api.Controllers [ApiController] [Route("{org}/{app}/v1/pages")] [AllowAnonymous] + [Obsolete("IPageOrder does not work with frontend version 4")] public class StatelessPagesController : ControllerBase { private readonly IAppModel _appModel; diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index aad701e8e..40d154cab 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -139,7 +139,9 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati 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 services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); diff --git a/src/Altinn.App.Core/Features/IPageOrder.cs b/src/Altinn.App.Core/Features/IPageOrder.cs index b69ef056b..43b94890a 100644 --- a/src/Altinn.App.Core/Features/IPageOrder.cs +++ b/src/Altinn.App.Core/Features/IPageOrder.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Features /// /// Interface for page order handling in stateful apps /// + [Obsolete("IPageOrder does not work with frontend version 4")] public interface IPageOrder { /// diff --git a/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs b/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs index c3f868b6f..e38804728 100644 --- a/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs +++ b/src/Altinn.App.Core/Features/PageOrder/DefaultPageOrder.cs @@ -6,6 +6,7 @@ namespace Altinn.App.Core.Features.PageOrder /// /// Interface for page order handling in stateless apps /// + [Obsolete("IPageOrder does not work with frontend version 4")] public class DefaultPageOrder : IPageOrder { private readonly IAppResources _resources; From 3f3629faf2af70f7d020d0db34238a38be5feb77 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Wed, 15 Nov 2023 16:13:45 +0100 Subject: [PATCH 26/46] Adding [Serializable] in dotnet 8 causes warning (#344) --- .../Implementation/EformidlingDeliveryException.cs | 7 ------- .../Helpers/DataModel/DataModelException.cs | 4 ---- src/Altinn.App.Core/Helpers/PlatformHttpException.cs | 8 -------- src/Altinn.App.Core/Helpers/ServiceException.cs | 8 -------- .../Internal/App/ApplicationConfigException.cs | 10 ---------- .../ExpressionEvaluatorTypeErrorException.cs | 4 ---- .../Internal/Pdf/PdfGenerationException.cs | 9 --------- 7 files changed, 50 deletions(-) diff --git a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs index 5c4c77aa6..c4b57df12 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/EformidlingDeliveryException.cs @@ -4,7 +4,6 @@ /// Exception thrown when Eformidling is unable to process the message delivered to /// the integration point. /// - [Serializable] public class EformidlingDeliveryException : Exception { /// @@ -23,11 +22,5 @@ public EformidlingDeliveryException(string message, Exception inner) : base(message, inner) { } - - /// - protected EformidlingDeliveryException(System.Runtime.Serialization.SerializationInfo serializationInfo, System.Runtime.Serialization.StreamingContext streamingContext) - : base(serializationInfo, streamingContext) - { - } } } diff --git a/src/Altinn.App.Core/Helpers/DataModel/DataModelException.cs b/src/Altinn.App.Core/Helpers/DataModel/DataModelException.cs index c00ff9634..6aa37e379 100644 --- a/src/Altinn.App.Core/Helpers/DataModel/DataModelException.cs +++ b/src/Altinn.App.Core/Helpers/DataModel/DataModelException.cs @@ -5,12 +5,8 @@ namespace Altinn.App.Core.Helpers.DataModel; /// /// Custom exception for errors when reading from a datamodel /// -[Serializable] public class DataModelException : Exception { /// public DataModelException(string msg): base(msg) { } - - /// - protected DataModelException(SerializationInfo info, StreamingContext ctxt) : base(info, ctxt) { } } \ No newline at end of file diff --git a/src/Altinn.App.Core/Helpers/PlatformHttpException.cs b/src/Altinn.App.Core/Helpers/PlatformHttpException.cs index 16c57703f..58911cf51 100644 --- a/src/Altinn.App.Core/Helpers/PlatformHttpException.cs +++ b/src/Altinn.App.Core/Helpers/PlatformHttpException.cs @@ -5,7 +5,6 @@ namespace Altinn.App.Core.Helpers /// /// Exception class to hold exceptions when talking to the platform REST services /// - [Serializable] public class PlatformHttpException : Exception { /// @@ -36,12 +35,5 @@ public PlatformHttpException(HttpResponseMessage response, string message) : bas { this.Response = response; } - - /// - /// Add serialization info. - /// - protected PlatformHttpException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } } } diff --git a/src/Altinn.App.Core/Helpers/ServiceException.cs b/src/Altinn.App.Core/Helpers/ServiceException.cs index 08b2e4a59..04bfc1e3b 100644 --- a/src/Altinn.App.Core/Helpers/ServiceException.cs +++ b/src/Altinn.App.Core/Helpers/ServiceException.cs @@ -6,7 +6,6 @@ namespace Altinn.App.Core.Helpers /// /// Exception that is thrown by service implementation. /// - [Serializable] public class ServiceException : Exception { /// @@ -34,12 +33,5 @@ public ServiceException(HttpStatusCode statusCode, string message, Exception inn { StatusCode = statusCode; } - - /// - /// Set serialization info. - /// - protected ServiceException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } } } diff --git a/src/Altinn.App.Core/Internal/App/ApplicationConfigException.cs b/src/Altinn.App.Core/Internal/App/ApplicationConfigException.cs index f89490331..e04d91078 100644 --- a/src/Altinn.App.Core/Internal/App/ApplicationConfigException.cs +++ b/src/Altinn.App.Core/Internal/App/ApplicationConfigException.cs @@ -5,7 +5,6 @@ namespace Altinn.App.Core.Internal.App /// /// Configuration is not valid for application /// - [Serializable] public class ApplicationConfigException: Exception { @@ -16,15 +15,6 @@ public ApplicationConfigException() { } - /// - /// Create ApplicationConfigException - /// - /// Exception information - /// Exception context - protected ApplicationConfigException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - /// /// Create ApplicationConfigException /// diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluatorTypeErrorException.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluatorTypeErrorException.cs index 4d86b1a75..bcb87bc3e 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluatorTypeErrorException.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluatorTypeErrorException.cs @@ -5,14 +5,10 @@ namespace Altinn.App.Core.Internal.Expressions; /// /// Custom exception for to thow when expressions contains type errors. /// -[Serializable] public class ExpressionEvaluatorTypeErrorException : Exception { /// public ExpressionEvaluatorTypeErrorException(string msg) : base(msg) {} /// public ExpressionEvaluatorTypeErrorException(string msg, Exception innerException) : base(msg, innerException) {} - - /// - protected ExpressionEvaluatorTypeErrorException(SerializationInfo info, StreamingContext ctxt) : base(info, ctxt) { } } \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs b/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs index 331c66d25..6daf76420 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfGenerationException.cs @@ -5,7 +5,6 @@ namespace Altinn.App.Core.Internal.Pdf /// /// Class representing an exception throw when a PDF could not be created. /// - [Serializable] public class PdfGenerationException : Exception { /// @@ -31,13 +30,5 @@ public PdfGenerationException(string? message) : base(message) public PdfGenerationException(string? message, Exception? innerException) : base(message, innerException) { } - - /// - /// Creates a new Exception of - /// Intended to be used when the generation of PDF fails. - /// - protected PdfGenerationException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } } } From 0fe6d08ad74ab242b2f95c0595c1a59958b6ddc0 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Thu, 16 Nov 2023 07:46:24 +0100 Subject: [PATCH 27/46] Update app-lib-dotnet to dotnet8 with c# 12 (#340) * Update app-lib-dotnet to dotnet8 with c# 12 * use net8 for codeql --- .github/workflows/codeql.yml | 10 ++++++++++ .github/workflows/dotnet-test.yml | 3 +-- .github/workflows/publish-release.yml | 4 ++-- .github/workflows/test-and-analyze-fork.yml | 3 +-- .github/workflows/test-and-analyze.yml | 3 +-- src/Altinn.App.Api/Altinn.App.Api.csproj | 4 ++-- src/Altinn.App.Core/Altinn.App.Core.csproj | 4 ++-- src/Directory.Build.props | 2 +- test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj | 2 +- .../Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj | 4 ++-- .../Internal/App/FrontendFeaturesTest.cs | 2 +- 11 files changed, 24 insertions(+), 17 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bc8594658..3e616e95f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -26,6 +26,16 @@ jobs: # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: + # The following step is required in order to use .NET 8 pre-release. + # We can remove if using an officially supported .NET version. + # See https://github.com/github/codeql-action/issues/757#issuecomment-977546999 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + include-prerelease: true + - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index 050821296..e2749d5ac 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -20,8 +20,7 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 6.0.x - 5.0.x + 8.0.x - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index cf7f351a4..6e927c18d 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -18,7 +18,7 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 6.0.x + 8.0.x - name: Install deps run: | dotnet restore @@ -48,7 +48,7 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 6.0.x + 8.0.x - name: Build bundles run: | make bundles diff --git a/.github/workflows/test-and-analyze-fork.yml b/.github/workflows/test-and-analyze-fork.yml index df4ca1969..a0c62e349 100644 --- a/.github/workflows/test-and-analyze-fork.yml +++ b/.github/workflows/test-and-analyze-fork.yml @@ -13,8 +13,7 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 6.0.x - 5.0.x + 8.0.x - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis diff --git a/.github/workflows/test-and-analyze.yml b/.github/workflows/test-and-analyze.yml index c3517dfaf..37398355c 100644 --- a/.github/workflows/test-and-analyze.yml +++ b/.github/workflows/test-and-analyze.yml @@ -19,8 +19,7 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: | - 6.0.x - 5.0.x + 8.0.x - name: Set up JDK 11 uses: actions/setup-java@v3 with: diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index 3bb643875..decbd1d50 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 Library Altinn.App.Api Altinn;Studio;App;Api;Controllers @@ -22,7 +22,7 @@ - + diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index a4de15977..7123c0642 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 enable enable @@ -18,7 +18,7 @@ - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c8d35e391..b1f3f0a3c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -10,7 +10,7 @@ preview.0 v true - 10.0 + 12 diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj index ccfd2ce52..cc3a1dbd4 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable false diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index 90a88f825..43ca355e0 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false @@ -42,7 +42,7 @@ - + diff --git a/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs b/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs index c34a386c2..1efb76060 100644 --- a/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/App/FrontendFeaturesTest.cs @@ -36,7 +36,7 @@ public async Task GetFeatures_returns_list_of_enabled_features_when_feature_flag { "jsonObjectInDataResponse", true }, }; var featureManagerMock = new Mock(); - featureManagerMock.Setup(f => f.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse, default)).ReturnsAsync(true); + featureManagerMock.Setup(f => f.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse)).ReturnsAsync(true); IFrontendFeatures frontendFeatures = new FrontendFeatures(featureManagerMock.Object); var actual = await frontendFeatures.GetFrontendFeatures(); actual.Should().BeEquivalentTo(expected); From b656bedbe280780c73c3c3272adbce4e4529e9c8 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Wed, 22 Nov 2023 10:10:23 +0100 Subject: [PATCH 28/46] Obsolete IText interface for getting texts from storage (#348) --- src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs | 2 ++ .../Infrastructure/Clients/Storage/TextClient.cs | 1 + src/Altinn.App.Core/Internal/Texts/IText.cs | 1 + 3 files changed, 4 insertions(+) diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 40d154cab..9285ba537 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -87,7 +87,9 @@ public static void AddPlatformServices(this IServiceCollection services, IConfig services.AddHttpClient(); services.Decorate(); services.AddHttpClient(); +#pragma warning disable CS0618 // Type or member is obsolete services.AddHttpClient(); +#pragma warning restore CS0618 // Type or member is obsolete services.AddHttpClient(); services.AddHttpClient(); diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs index 5653b5a75..e2603e101 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs @@ -16,6 +16,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage /// /// A client forretrieving text resources from Altinn Platform. /// + [Obsolete("Use IAppResources.GetTexts() instead")] public class TextClient : IText { private readonly ILogger _logger; diff --git a/src/Altinn.App.Core/Internal/Texts/IText.cs b/src/Altinn.App.Core/Internal/Texts/IText.cs index 67c592564..0364e88d0 100644 --- a/src/Altinn.App.Core/Internal/Texts/IText.cs +++ b/src/Altinn.App.Core/Internal/Texts/IText.cs @@ -5,6 +5,7 @@ namespace Altinn.App.Core.Internal.Texts /// /// Describes the public methods of a text resources service /// + [Obsolete("Use IAppResources.GetTexts() instead")] public interface IText { /// From d800c08aacea5acb8f7d73d25c3cbb4dbf19b51e Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Thu, 23 Nov 2023 23:00:25 +0100 Subject: [PATCH 29/46] Fix a few more nullability issues (#347) --- .github/workflows/publish-release.yml | 4 ++-- src/Altinn.App.Api/Controllers/InstancesController.cs | 6 +++--- src/Altinn.App.Core/Altinn.App.Core.csproj | 6 +++--- .../Infrastructure/Clients/Register/AltinnPartyClient.cs | 2 +- .../Infrastructure/Clients/Storage/TextClient.cs | 4 ++-- .../Internal/Registers/IAltinnPartyClient.cs | 2 +- src/Altinn.App.Core/Internal/Texts/IText.cs | 2 +- test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj | 8 ++++---- .../Altinn.App.Common.Tests.csproj | 4 ++-- test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj | 4 ++-- .../DataLists/NullDataListProviderTest.cs | 5 +++-- 11 files changed, 24 insertions(+), 23 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 6e927c18d..3af06de13 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -14,7 +14,7 @@ jobs: with: fetch-depth: 0 - - name: Install dotnet6 + - name: Install dotnet8 uses: actions/setup-dotnet@v3 with: dotnet-version: | @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Install dotnet6 + - name: Install dotnet8 uses: actions/setup-dotnet@v3 with: dotnet-version: | diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index 38a75af7e..c6fbe0f87 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -235,7 +235,7 @@ public async Task> Post( Party party; try { - party = await LookupParty(instanceTemplate.InstanceOwner); + party = await LookupParty(instanceTemplate.InstanceOwner) ?? throw new Exception("Unknown party"); instanceTemplate.InstanceOwner = InstantiationHelper.PartyToInstanceOwner(party); } catch (Exception partyLookupException) @@ -378,7 +378,7 @@ public async Task> PostSimplified( Party party; try { - party = await LookupParty(instansiationInstance.InstanceOwner); + party = await LookupParty(instansiationInstance.InstanceOwner) ?? throw new Exception("Unknown party"); instansiationInstance.InstanceOwner = InstantiationHelper.PartyToInstanceOwner(party); } catch (Exception partyLookupException) @@ -901,7 +901,7 @@ private async Task AuthorizeAction(string org, string app, in return enforcementResult; } - private async Task LookupParty(InstanceOwner instanceOwner) + private async Task LookupParty(InstanceOwner instanceOwner) { if (instanceOwner.PartyId != null) { diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index 7123c0642..49b80a61f 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -1,4 +1,4 @@ - + net8.0 enable @@ -9,12 +9,12 @@ - + - + diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs index b7c90cbfa..fece3e574 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs @@ -60,7 +60,7 @@ public AltinnPartyClient( } /// - public async Task GetParty(int partyId) + public async Task GetParty(int partyId) { Party? party = null; diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs index e2603e101..911f38e8c 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs @@ -59,9 +59,9 @@ public TextClient( } /// - public async Task GetText(string org, string app, string language) + public async Task GetText(string org, string app, string language) { - TextResource textResource = null; + TextResource? textResource = null; string cacheKey = $"{org}-{app}-{language.ToLower()}"; if (!_memoryCache.TryGetValue(cacheKey, out textResource)) diff --git a/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs b/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs index d1e4e12c6..b6c38ecdb 100644 --- a/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs +++ b/src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs @@ -12,7 +12,7 @@ public interface IAltinnPartyClient /// /// The partyId /// The party for the given partyId - Task GetParty(int partyId); + Task GetParty(int partyId); /// /// Looks up a party by person or organisation number. diff --git a/src/Altinn.App.Core/Internal/Texts/IText.cs b/src/Altinn.App.Core/Internal/Texts/IText.cs index 0364e88d0..b4851b8e0 100644 --- a/src/Altinn.App.Core/Internal/Texts/IText.cs +++ b/src/Altinn.App.Core/Internal/Texts/IText.cs @@ -15,6 +15,6 @@ public interface IText /// Application identifier which is unique within an organisation. /// Language for the text resource /// The text resource - Task GetText(string org, string app, string language); + Task GetText(string org, string app, string language); } } diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj index cc3a1dbd4..9f797b19a 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -14,12 +14,12 @@ - - + + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj index ca11a7812..44eda7035 100644 --- a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj +++ b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj @@ -26,8 +26,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index 43ca355e0..a5a21bde6 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -45,8 +45,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs b/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs index 17b996cc7..fca68fbe6 100644 --- a/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs +++ b/test/Altinn.App.Core.Tests/DataLists/NullDataListProviderTest.cs @@ -12,12 +12,13 @@ namespace Altinn.App.PlatformServices.Tests.DataLists public class NullDataListProviderTest { [Fact] - public void Constructor_InitializedWithEmptyValues() + public async Task Constructor_InitializedWithEmptyValues() { var provider = new NullDataListProvider(); provider.Id.Should().Be(string.Empty); - provider.GetDataListAsync("nb", new Dictionary()).Result.ListItems.Should().BeNull(); + var list = await provider.GetDataListAsync("nb", new Dictionary()); + list.ListItems.Should().BeNull(); } } } \ No newline at end of file From eb6ecdd209d0f0fba1494e4b30d4f87141acf135 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Fri, 24 Nov 2023 09:22:00 +0100 Subject: [PATCH 30/46] Add all test data from localtest and add a few missing mocks (#362) --- .../tdd/contributer-restriction/.gitignore | 6 + .../Data/Profile/User/1001.json | 13 ++ .../Data/Profile/User/1002.json | 13 ++ .../Data/Profile/User/1003.json | 13 ++ .../Data/Profile/User/12345.json | 13 ++ .../Data/Profile/User/1337.json | 13 ++ .../Data/Register/Org/897069631.json | 16 +++ .../Data/Register/Org/897069650.json | 16 +++ .../Data/Register/Org/897069651.json | 16 +++ .../Data/Register/Org/897069652.json | 16 +++ .../Data/Register/Org/897069653.json | 16 +++ .../Data/Register/Org/900000001.json | 16 +++ .../Data/Register/Org/910423185.json | 16 +++ .../Data/Register/Org/910423495.json | 16 +++ .../Data/Register/Org/910457292.json | 16 +++ .../Data/Register/Org/910471120.json | 16 +++ .../Data/Register/Org/950474084.json | 16 +++ .../Data/Register/Party/500000.json | 13 ++ .../Data/Register/Party/500001.json | 13 ++ .../Data/Register/Party/500002.json | 13 ++ .../Data/Register/Party/500003.json | 13 ++ .../Data/Register/Party/500600.json | 13 ++ .../Data/Register/Party/500700.json | 13 ++ .../Data/Register/Party/500800.json | 13 ++ .../Data/Register/Party/500801.json | 13 ++ .../Data/Register/Party/500802.json | 13 ++ .../Data/Register/Party/501337.json | 13 ++ .../Data/Register/Party/510001.json | 14 ++ .../Data/Register/Party/510002.json | 14 ++ .../Data/Register/Party/510003.json | 14 ++ .../Data/Register/Party/512345.json | 13 ++ .../Data/Register/Person/01017512345.json | 19 +++ .../Data/Register/Person/01039012345.json | 19 +++ .../Data/Register/Person/01899699552.json | 20 +++ .../Data/Register/Person/08829698278.json | 20 +++ .../Data/Register/Person/17858296439.json | 20 +++ test/Altinn.App.Api.Tests/Data/TestData.cs | 14 +- .../Data/authorization/claims/12345.json | 7 + .../Data/authorization/claims/1337.json | 7 + .../Data/authorization/partylist/1001.json | 25 ++++ .../Data/authorization/partylist/1002.json | 25 ++++ .../Data/authorization/partylist/1003.json | 13 ++ .../Data/authorization/partylist/12345.json | 13 ++ .../Data/authorization/partylist/1337.json | 124 ++++++++++++++++++ .../authorization/resources/Appid_119.json | 56 ++++++++ .../authorization/resources/Appid_120.json | 56 ++++++++ .../authorization/resources/Appid_122.json | 56 ++++++++ .../authorization/resources/Appid_123.json | 65 +++++++++ .../authorization/resources/Appid_124.json | 56 ++++++++ .../authorization/resources/Appid_125.json | 56 ++++++++ .../authorization/resources/Appid_126.json | 56 ++++++++ .../authorization/resources/Appid_127.json | 56 ++++++++ .../authorization/resources/Appid_128.json | 56 ++++++++ .../authorization/resources/Appid_129.json | 56 ++++++++ .../authorization/resources/Appid_130.json | 56 ++++++++ .../authorization/resources/Appid_132.json | 56 ++++++++ .../authorization/resources/Appid_133.json | 56 ++++++++ .../authorization/resources/Appid_134.json | 56 ++++++++ .../authorization/resources/Appid_136.json | 56 ++++++++ .../authorization/resources/Appid_137.json | 56 ++++++++ .../authorization/resources/Appid_138.json | 56 ++++++++ .../authorization/resources/Appid_139.json | 56 ++++++++ .../authorization/resources/Appid_142.json | 56 ++++++++ .../authorization/resources/Appid_144.json | 56 ++++++++ .../authorization/resources/Appid_145.json | 56 ++++++++ .../authorization/resources/Appid_147.json | 56 ++++++++ .../authorization/resources/Appid_148.json | 56 ++++++++ .../authorization/resources/Appid_150.json | 56 ++++++++ .../authorization/resources/Appid_153.json | 56 ++++++++ .../authorization/resources/Appid_154.json | 56 ++++++++ .../authorization/resources/Appid_155.json | 56 ++++++++ .../authorization/resources/Appid_164.json | 56 ++++++++ .../authorization/resources/Appid_168.json | 56 ++++++++ .../authorization/resources/Appid_178.json | 56 ++++++++ .../authorization/resources/Appid_179.json | 56 ++++++++ .../authorization/resources/Appid_180.json | 56 ++++++++ .../authorization/resources/Appid_181.json | 56 ++++++++ .../authorization/resources/Appid_182.json | 56 ++++++++ .../authorization/resources/Appid_184.json | 56 ++++++++ .../authorization/resources/Appid_185.json | 56 ++++++++ .../authorization/resources/Appid_191.json | 56 ++++++++ .../authorization/resources/Appid_192.json | 56 ++++++++ .../authorization/resources/Appid_193.json | 56 ++++++++ .../authorization/resources/Appid_196.json | 56 ++++++++ .../authorization/resources/Appid_197.json | 56 ++++++++ .../authorization/resources/Appid_198.json | 56 ++++++++ .../authorization/resources/Appid_199.json | 56 ++++++++ .../authorization/resources/Appid_200.json | 56 ++++++++ .../authorization/resources/Appid_201.json | 41 ++++++ .../authorization/resources/Appid_202.json | 38 ++++++ .../authorization/resources/Appid_203.json | 56 ++++++++ .../authorization/resources/Appid_204.json | 38 ++++++ .../authorization/resources/Appid_205.json | 56 ++++++++ .../authorization/resources/Appid_206.json | 56 ++++++++ .../authorization/resources/Appid_207.json | 56 ++++++++ .../authorization/resources/Appid_208.json | 54 ++++++++ .../authorization/resources/Appid_209.json | 56 ++++++++ .../authorization/resources/Appid_210.json | 56 ++++++++ .../authorization/resources/Appid_211.json | 56 ++++++++ .../authorization/resources/Appid_212.json | 56 ++++++++ .../authorization/resources/Appid_213.json | 52 ++++++++ .../authorization/resources/Appid_214.json | 52 ++++++++ .../authorization/resources/Appid_215.json | 52 ++++++++ .../authorization/resources/Appid_216.json | 52 ++++++++ .../authorization/resources/Appid_217.json | 56 ++++++++ .../authorization/resources/Appid_218.json | 56 ++++++++ .../authorization/resources/Appid_219.json | 38 ++++++ .../authorization/resources/Appid_220.json | 38 ++++++ .../authorization/resources/Appid_221.json | 38 ++++++ .../authorization/resources/Appid_222.json | 38 ++++++ .../authorization/resources/Appid_223.json | 38 ++++++ .../authorization/resources/Appid_400.json | 60 +++++++++ .../authorization/resources/Appid_401.json | 65 +++++++++ .../authorization/resources/Appid_402.json | 65 +++++++++ .../authorization/resources/Appid_403.json | 60 +++++++++ .../authorization/resources/Appid_43.json | 60 +++++++++ .../resources/altinn_access_management.json | 26 ++++ .../nav_tiltakAvtaleOmArbeidstrening.json | 46 +++++++ .../altinn_maskinporten_scope_delegation.xml | 52 ++++++++ .../roles/User_1001/party_500000/roles.json | 14 ++ .../party_510001}/roles.json | 6 +- .../party_500000}/roles.json | 5 +- .../roles/User_1002/party_510002/roles.json | 16 +++ .../roles/User_1003/party_510003/roles.json | 16 +++ .../{party_12345 => party_512345}/roles.json | 4 + .../roles/User_1337/party_1404/roles.json | 67 ---------- .../roles/User_1337/party_500000/roles.json | 12 +- .../roles/User_1337/party_500800/roles.json | 18 +++ .../roles/User_1337/party_500801/roles.json | 18 +++ .../roles/User_1337/party_500802/roles.json | 18 +++ .../{party_1337 => party_501337}/roles.json | 0 .../roles/user_1/party_1002/roles.json | 11 -- .../roles/user_2/party_1000/roles.json | 7 - .../Mocks/AltinnPartyClientMock.cs | 45 +++++++ .../Mocks/AppModelMock.cs | 18 +++ .../Mocks/Event/InstanceEventClientMock.cs | 18 +++ .../Mocks/ProfileClientMock.cs | 20 +++ test/Altinn.App.Api.Tests/Program.cs | 7 + 138 files changed, 5018 insertions(+), 96 deletions(-) create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/.gitignore create mode 100644 test/Altinn.App.Api.Tests/Data/Profile/User/1001.json create mode 100644 test/Altinn.App.Api.Tests/Data/Profile/User/1002.json create mode 100644 test/Altinn.App.Api.Tests/Data/Profile/User/1003.json create mode 100644 test/Altinn.App.Api.Tests/Data/Profile/User/12345.json create mode 100644 test/Altinn.App.Api.Tests/Data/Profile/User/1337.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Org/897069631.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Org/897069650.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Org/897069651.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Org/897069652.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Org/897069653.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Org/900000001.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Org/910423185.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Org/910423495.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Org/910457292.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Org/910471120.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Org/950474084.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/500000.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/500001.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/500002.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/500003.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/500600.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/500700.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/500800.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/500801.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/500802.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/501337.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/510001.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/510002.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/510003.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/512345.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Person/01017512345.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Person/01039012345.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Person/01899699552.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Person/08829698278.json create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Person/17858296439.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/claims/12345.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/claims/1337.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/partylist/1001.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/partylist/1002.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/partylist/1003.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/partylist/12345.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/partylist/1337.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_119.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_120.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_122.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_123.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_124.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_125.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_126.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_127.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_128.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_129.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_130.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_132.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_133.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_134.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_136.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_137.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_138.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_139.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_142.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_144.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_145.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_147.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_148.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_150.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_153.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_154.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_155.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_164.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_168.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_178.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_179.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_180.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_181.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_182.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_184.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_185.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_191.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_192.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_193.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_196.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_197.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_198.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_199.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_200.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_201.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_202.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_203.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_204.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_205.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_206.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_207.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_208.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_209.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_210.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_211.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_212.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_213.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_214.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_215.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_216.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_217.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_218.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_219.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_220.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_221.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_222.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_223.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_400.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_401.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_402.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_403.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_43.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/altinn_access_management.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/nav_tiltakAvtaleOmArbeidstrening.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/resources/policies/altinn_maskinporten_scope_delegation.xml create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_500000/roles.json rename test/Altinn.App.Api.Tests/Data/authorization/roles/{user_3/party_1003 => User_1001/party_510001}/roles.json (66%) rename test/Altinn.App.Api.Tests/Data/authorization/roles/{user_1/party_1000 => User_1002/party_500000}/roles.json (60%) create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_510002/roles.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/roles/User_1003/party_510003/roles.json rename test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/{party_12345 => party_512345}/roles.json (67%) delete mode 100644 test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_1404/roles.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500800/roles.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500801/roles.json create mode 100644 test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500802/roles.json rename test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/{party_1337 => party_501337}/roles.json (100%) delete mode 100644 test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1002/roles.json delete mode 100644 test/Altinn.App.Api.Tests/Data/authorization/roles/user_2/party_1000/roles.json create mode 100644 test/Altinn.App.Api.Tests/Mocks/AltinnPartyClientMock.cs create mode 100644 test/Altinn.App.Api.Tests/Mocks/AppModelMock.cs create mode 100644 test/Altinn.App.Api.Tests/Mocks/Event/InstanceEventClientMock.cs create mode 100644 test/Altinn.App.Api.Tests/Mocks/ProfileClientMock.cs diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/.gitignore b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/.gitignore new file mode 100644 index 000000000..9c7e6c4af --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/.gitignore @@ -0,0 +1,6 @@ +#Ignore all blob files named with a guid for the dataElementId +????????-????-????-????-???????????? +# Ignore json files +*.json +# Except those that ends in .pretest.json +!*.pretest.json \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Profile/User/1001.json b/test/Altinn.App.Api.Tests/Data/Profile/User/1001.json new file mode 100644 index 000000000..4338605a9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Profile/User/1001.json @@ -0,0 +1,13 @@ +{ + "UserId": 1001, + "UserName": "PengelensPartner", + "PhoneNumber": "12345678", + "Email": "test@test.com", + "PartyId": 510001, + "Party": {}, + "UserType": 0, + "ProfileSettingPreference": { + "Language": "nb", + "doNotPromptForParty": true + } +} diff --git a/test/Altinn.App.Api.Tests/Data/Profile/User/1002.json b/test/Altinn.App.Api.Tests/Data/Profile/User/1002.json new file mode 100644 index 000000000..a7767b65d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Profile/User/1002.json @@ -0,0 +1,13 @@ +{ + "UserId": 1002, + "UserName": "GjentagendeForelder", + "PhoneNumber": "12345678", + "Email": "test@test.com", + "PartyId": 510002, + "Party": {}, + "UserType": 0, + "ProfileSettingPreference": { + "Language": "nb", + "doNotPromptForParty": true + } +} diff --git a/test/Altinn.App.Api.Tests/Data/Profile/User/1003.json b/test/Altinn.App.Api.Tests/Data/Profile/User/1003.json new file mode 100644 index 000000000..b04e2df1d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Profile/User/1003.json @@ -0,0 +1,13 @@ +{ + "UserId": 1003, + "UserName": "RikForelder", + "PhoneNumber": "12345678", + "Email": "test@test.com", + "PartyId": 510003, + "Party": {}, + "UserType": 0, + "ProfileSettingPreference": { + "Language": "nb", + "doNotPromptForParty": true + } +} diff --git a/test/Altinn.App.Api.Tests/Data/Profile/User/12345.json b/test/Altinn.App.Api.Tests/Data/Profile/User/12345.json new file mode 100644 index 000000000..d1ad5db12 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Profile/User/12345.json @@ -0,0 +1,13 @@ +{ + "UserId": 12345, + "UserName": "OlaNordmann", + "PhoneNumber": "12345678", + "Email": "test@test.com", + "PartyId": 512345, + "Party": {}, + "UserType": 0, + "ProfileSettingPreference": { + "Language": "nb", + "doNotPromptForParty": true + } +} diff --git a/test/Altinn.App.Api.Tests/Data/Profile/User/1337.json b/test/Altinn.App.Api.Tests/Data/Profile/User/1337.json new file mode 100644 index 000000000..7624c62be --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Profile/User/1337.json @@ -0,0 +1,13 @@ +{ + "UserId": 1337, + "UserName": "SophieDDG", + "PhoneNumber": "90001337", + "Email": "1337@altinnstudiotestusers.com", + "PartyId": 501337, + "Party": {}, + "UserType": 1, + "ProfileSettingPreference": { + "Language": "nn", + "doNotPromptForParty": true + } +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/897069631.json b/test/Altinn.App.Api.Tests/Data/Register/Org/897069631.json new file mode 100644 index 000000000..e4cc66b51 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/897069631.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "897069631", + "Name": "EAS Health Consulting", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "epost@setra.no", + "InternetAddress": "http://setrabrl.no", + "MailingAddress": "Sofies Gate 2", + "MailingPostalCode": "0170", + "MailingPostalCity": "Oslo", + "BusinessAddress": "Sofies Gate 2", + "BusinessPostalCode": "0170", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/897069650.json b/test/Altinn.App.Api.Tests/Data/Register/Org/897069650.json new file mode 100644 index 000000000..8c0fc65a7 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/897069650.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "897069650", + "Name": "DDG Fitness", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "central@ddgfitness.no", + "InternetAddress": "http://ddgfitness.no", + "MailingAddress": "Sofies Gate 1", + "MailingPostalCode": "0170", + "MailingPostalCity": "Oslo", + "BusinessAddress": "Sofies Gate 1", + "BusinessPostalCode": "0170", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/897069651.json b/test/Altinn.App.Api.Tests/Data/Register/Org/897069651.json new file mode 100644 index 000000000..ee5880e56 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/897069651.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "897069651", + "Name": "DDG Fitness Oslo", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010001", + "FaxNumber": "92110001", + "EMailAddress": "oslo@ddgfitness.no", + "InternetAddress": "http://ddgfitness.no", + "MailingAddress": "Sofies Gate 1", + "MailingPostalCode": "0170", + "MailingPostalCity": "Oslo", + "BusinessAddress": "Sofies Gate 1", + "BusinessPostalCode": "0170", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/897069652.json b/test/Altinn.App.Api.Tests/Data/Register/Org/897069652.json new file mode 100644 index 000000000..2e0c75d63 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/897069652.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "897069652", + "Name": "DDG Fitness Bergen", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010002", + "FaxNumber": "92110002", + "EMailAddress": "bergen@ddgfitness.no", + "InternetAddress": "http://ddgfitness.no", + "MailingAddress": "Olav Kyrres Gate 11", + "MailingPostalCode": "5014", + "MailingPostalCity": "Bergen", + "BusinessAddress": "Olav Kyrres Gate 11", + "BusinessPostalCode": "5014", + "BusinessPostalCity": "Bergen" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/897069653.json b/test/Altinn.App.Api.Tests/Data/Register/Org/897069653.json new file mode 100644 index 000000000..787630812 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/897069653.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "897069653", + "Name": "DDG Fitness Trondheim", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010003", + "FaxNumber": "92110003", + "EMailAddress": "trondheim@ddgfitness.no", + "InternetAddress": "http://ddgfitness.no", + "MailingAddress": "Kjøpmannsgata 25", + "MailingPostalCode": "7013", + "MailingPostalCity": "Trondheim", + "BusinessAddress": "Kjøpmannsgata 25", + "BusinessPostalCode": "7013", + "BusinessPostalCity": "Trondheim" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/900000001.json b/test/Altinn.App.Api.Tests/Data/Register/Org/900000001.json new file mode 100644 index 000000000..717083744 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/900000001.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "900000001", + "Name": "Kari Consulting", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "1234578", + "FaxNumber": "12345678", + "EMailAddress": "email@email.com", + "InternetAddress": "http://example.com", + "MailingAddress": "Postadresse 9", + "MailingPostalCode": "0000", + "MailingPostalCity": "By", + "BusinessAddress": "Forretningsadresse 9", + "BusinessPostalCode": "0000", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/910423185.json b/test/Altinn.App.Api.Tests/Data/Register/Org/910423185.json new file mode 100644 index 000000000..5e226b296 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/910423185.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "910423185", + "Name": "EAS Health Consulting Svolvær", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "lofoten@eashealt.no", + "InternetAddress": "http://eashealt.no", + "MailingAddress": "Feskslogveien 12", + "MailingPostalCode": "8400", + "MailingPostalCity": "Svolvær", + "BusinessAddress": "Feskslogveien 12", + "BusinessPostalCode": "8300", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/910423495.json b/test/Altinn.App.Api.Tests/Data/Register/Org/910423495.json new file mode 100644 index 000000000..ee996843c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/910423495.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "923609016", + "Name": "EAS Health Consulting Svolvær", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "lofoten@eashealt.no", + "InternetAddress": "http://eashealt.no", + "MailingAddress": "Feskslogveien 12", + "MailingPostalCode": "8400", + "MailingPostalCity": "Svolvær", + "BusinessAddress": "Feskslogveien 12", + "BusinessPostalCode": "8300", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/910457292.json b/test/Altinn.App.Api.Tests/Data/Register/Org/910457292.json new file mode 100644 index 000000000..8b1622355 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/910457292.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "910457292", + "Name": "EAS Health Consulting Sortland", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "sortland@eashealt.no", + "InternetAddress": "http://setrabrl.no", + "MailingAddress": "Strandgata 2", + "MailingPostalCode": "8400", + "MailingPostalCity": "Sortland", + "BusinessAddress": "Strandgata 2", + "BusinessPostalCode": "8400", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/910471120.json b/test/Altinn.App.Api.Tests/Data/Register/Org/910471120.json new file mode 100644 index 000000000..2d1b061ed --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/910471120.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "910471120", + "Name": "EAS Health Consulting Stokmarknes", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "sortland@eashealt.no", + "InternetAddress": "http://setrabrl.no", + "MailingAddress": "Strandgata 2", + "MailingPostalCode": "8450", + "MailingPostalCity": "Sortland", + "BusinessAddress": "Strandgata 2", + "BusinessPostalCode": "8450", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Org/950474084.json b/test/Altinn.App.Api.Tests/Data/Register/Org/950474084.json new file mode 100644 index 000000000..14b9aa8c5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Org/950474084.json @@ -0,0 +1,16 @@ +{ + "OrgNumber": "950474084", + "Name": "Oslos Vakreste Borettslag", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "92010000", + "FaxNumber": "92110000", + "EMailAddress": "epost@setra.no", + "InternetAddress": "http://setrabrl.no", + "MailingAddress": "Sofies Gate 2", + "MailingPostalCode": "0170", + "MailingPostalCity": "Oslo", + "BusinessAddress": "Sofies Gate 2", + "BusinessPostalCode": "0170", + "BusinessPostalCity": "By" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500000.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500000.json new file mode 100644 index 000000000..f51d60eb0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500000.json @@ -0,0 +1,13 @@ +{ + "partyId": "500000", + "partyTypeName": 2, + "orgNumber": "897069650", + "ssn": null, + "unitType": "AS", + "name": "DDG Fitness", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500001.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500001.json new file mode 100644 index 000000000..eccb6a29f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500001.json @@ -0,0 +1,13 @@ +{ + "partyId": "500001", + "partyTypeName": 2, + "orgNumber": "897069651", + "ssn": null, + "unitType": "BEDR", + "name": "DDG Fitness Oslo", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500002.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500002.json new file mode 100644 index 000000000..91b04a699 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500002.json @@ -0,0 +1,13 @@ +{ + "partyId": "500002", + "partyTypeName": 2, + "orgNumber": "897069652", + "ssn": null, + "unitType": "BEDR", + "name": "DDG Fitness Bergen", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500003.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500003.json new file mode 100644 index 000000000..0ba24c6b7 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500003.json @@ -0,0 +1,13 @@ +{ + "partyId": "500003", + "partyTypeName": 2, + "orgNumber": "897069653", + "ssn": null, + "unitType": "BEDR", + "name": "DDG Fitness Trondheim", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500600.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500600.json new file mode 100644 index 000000000..417a3a3df --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500600.json @@ -0,0 +1,13 @@ +{ + "partyId": "500600", + "partyTypeName": 2, + "orgNumber": "897069631", + "ssn": null, + "unitType": "AS", + "name": "EAS Health Consulting", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500700.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500700.json new file mode 100644 index 000000000..780fe3286 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500700.json @@ -0,0 +1,13 @@ +{ + "partyId": "500700", + "partyTypeName": 2, + "orgNumber": "950474084", + "ssn": null, + "unitType": "BRL", + "name": "Oslos Vakreste Borettslag", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500800.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500800.json new file mode 100644 index 000000000..ae3d85a3b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500800.json @@ -0,0 +1,13 @@ +{ + "partyId": "500800", + "partyTypeName": 2, + "orgNumber": "910457292", + "ssn": null, + "unitType": "AS", + "name": "EAS Health Consulting Sortland", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500801.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500801.json new file mode 100644 index 000000000..093ac8c9f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500801.json @@ -0,0 +1,13 @@ +{ + "partyId": "500801", + "partyTypeName": 2, + "orgNumber": "910471120", + "ssn": null, + "unitType": "AS", + "name": "EAS Health Consulting Stokmarknes", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/500802.json b/test/Altinn.App.Api.Tests/Data/Register/Party/500802.json new file mode 100644 index 000000000..e6eb31362 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/500802.json @@ -0,0 +1,13 @@ +{ + "partyId": "500802", + "partyTypeName": 2, + "orgNumber": "910423495", + "ssn": null, + "unitType": "AS", + "name": "EAS Health Consulting Svolvær", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/501337.json b/test/Altinn.App.Api.Tests/Data/Register/Party/501337.json new file mode 100644 index 000000000..e4da6b1a9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/501337.json @@ -0,0 +1,13 @@ +{ + "partyId": "501337", + "partyTypeName": 1, + "orgNumber": null, + "ssn": "01039012345", + "unitType": null, + "name": "Sophie Salt", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/510001.json b/test/Altinn.App.Api.Tests/Data/Register/Party/510001.json new file mode 100644 index 000000000..6649f64b5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/510001.json @@ -0,0 +1,14 @@ +{ + "partyId": "510001", + "partyTypeName": 1, + "orgNumber": null, + "ssn": "01899699552", + "unitType": null, + "name": "Pengelens Partner", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/510002.json b/test/Altinn.App.Api.Tests/Data/Register/Party/510002.json new file mode 100644 index 000000000..bf8f73ef1 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/510002.json @@ -0,0 +1,14 @@ +{ + "partyId": "510002", + "partyTypeName": 1, + "orgNumber": null, + "ssn": "17858296439", + "unitType": null, + "name": "Gjentagende Forelder", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/510003.json b/test/Altinn.App.Api.Tests/Data/Register/Party/510003.json new file mode 100644 index 000000000..20096f3ed --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/510003.json @@ -0,0 +1,14 @@ +{ + "partyId": "510003", + "partyTypeName": 1, + "orgNumber": null, + "ssn": "08829698278", + "unitType": null, + "name": "Rik Forelder", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/512345.json b/test/Altinn.App.Api.Tests/Data/Register/Party/512345.json new file mode 100644 index 000000000..3df3c4ae8 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/512345.json @@ -0,0 +1,13 @@ +{ + "partyId": "512345", + "partyTypeName": 1, + "orgNumber": null, + "ssn": "01017512345", + "unitType": null, + "name": "Ola Nordmann", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Person/01017512345.json b/test/Altinn.App.Api.Tests/Data/Register/Person/01017512345.json new file mode 100644 index 000000000..340821484 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Person/01017512345.json @@ -0,0 +1,19 @@ +{ + "SSN": "01017512345", + "Name": "Ola Nordmann", + "FirstName": "Ola", + "MiddleName": "", + "LastName": "Nordmann", + "TelephoneNumber": "12345678", + "MobileNumber": "87654321", + "MailingAddress": "Blåbæreveien 7", + "MailingPostalCode": "8450", + "MailingPostalCity": "Stokmarknes", + "AddressMunicipalNumber": "1866", + "AddressMunicipalName": "Hadsel", + "AddressStreetName": "Blåbærveien", + "AddressHouseNumber": "7", + "AddressHouseLetter": null, + "AddressPostalCode": "8450", + "AddressCity": "Stokarknes" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Person/01039012345.json b/test/Altinn.App.Api.Tests/Data/Register/Person/01039012345.json new file mode 100644 index 000000000..038533a6b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Person/01039012345.json @@ -0,0 +1,19 @@ +{ + "SSN": "01039012345", + "Name": "Sophie Salt", + "FirstName": "Sophie", + "MiddleName": "", + "LastName": "Salt", + "TelephoneNumber": "12345678", + "MobileNumber": "87654321", + "MailingAddress": "Grev Wedels Plass 9", + "MailingPostalCode": "0157", + "MailingPostalCity": "Oslo", + "AddressMunicipalNumber": "0301", + "AddressMunicipalName": "Oslo", + "AddressStreetName": "Grev Wedels Plass", + "AddressHouseNumber": "9", + "AddressHouseLetter": null, + "AddressPostalCode": "0151", + "AddressCity": "Oslo" +} diff --git a/test/Altinn.App.Api.Tests/Data/Register/Person/01899699552.json b/test/Altinn.App.Api.Tests/Data/Register/Person/01899699552.json new file mode 100644 index 000000000..08755c1fd --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Person/01899699552.json @@ -0,0 +1,20 @@ +{ + "SSN": "01899699552", + "Name": "Pengelens Partner", + "FirstName": "Pengelens", + "MiddleName": "", + "LastName": "Partner", + "TelephoneNumber": "12345678", + "MobileNumber": "87654321", + "MailingAddress": "Tuftekåsvegen 7", + "MailingPostalCode": "3920", + "MailingPostalCity": "PORSGRUNN", + "AddressMunicipalNumber": "3806", + "AddressMunicipalName": "Porsgrunn", + "AddressStreetName": "Tuftekåsvegen", + "AddressHouseNumber": "7", + "AddressHouseLetter": null, + "AddressPostalCode": "3920", + "AddressCity": "PORSGRUNN" + } + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Register/Person/08829698278.json b/test/Altinn.App.Api.Tests/Data/Register/Person/08829698278.json new file mode 100644 index 000000000..ca0ada01d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Person/08829698278.json @@ -0,0 +1,20 @@ +{ + "SSN": "08829698278", + "Name": "Rik Forelder", + "FirstName": "Rik", + "MiddleName": "", + "LastName": "Forelder", + "TelephoneNumber": "12345678", + "MobileNumber": "87654321", + "MailingAddress": "Bjønnkamvegen 14", + "MailingPostalCode": "3735", + "MailingPostalCity": "SKIEN", + "AddressMunicipalNumber": "3807", + "AddressMunicipalName": "Skien", + "AddressStreetName": "Bjønnkamvegen", + "AddressHouseNumber": "14", + "AddressHouseLetter": null, + "AddressPostalCode": "3735", + "AddressCity": "SKIEN" + } + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Register/Person/17858296439.json b/test/Altinn.App.Api.Tests/Data/Register/Person/17858296439.json new file mode 100644 index 000000000..38464d2ad --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Person/17858296439.json @@ -0,0 +1,20 @@ +{ + "SSN": "17858296439", + "Name": "Gjentagende Forelder", + "FirstName": "Gjentagende", + "MiddleName": "", + "LastName": "Forelder", + "TelephoneNumber": "12345678", + "MobileNumber": "87654321", + "MailingAddress": "Ørneveien 36", + "MailingPostalCode": "1640", + "MailingPostalCity": "RÅDE", + "AddressMunicipalNumber": "3017", + "AddressMunicipalName": "Råde", + "AddressStreetName": "Ørneveien", + "AddressHouseNumber": "36", + "AddressHouseLetter": null, + "AddressPostalCode": "1640", + "AddressCity": "RÅDE" + } + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/TestData.cs b/test/Altinn.App.Api.Tests/Data/TestData.cs index e67c0cd33..d7f7acca8 100644 --- a/test/Altinn.App.Api.Tests/Data/TestData.cs +++ b/test/Altinn.App.Api.Tests/Data/TestData.cs @@ -73,6 +73,18 @@ public static string GetAltinnAppsPolicyPath(string org, string app) return Path.Combine(testDataDirectory, "apps", org, app, "config", "authorization") + Path.DirectorySeparatorChar; } + public static string GetAltinnProfilePath() + { + string testDataDirectory = GetTestDataRootDirectory(); + return Path.Combine(testDataDirectory, "Register", "Party"); + } + + public static string GetRegisterProfilePath() + { + string testDataDirectory = GetTestDataRootDirectory(); + return Path.Combine(testDataDirectory, "Profile", "User"); + } + public static void DeleteInstance(string org, string app, int instanceOwnerId, Guid instanceGuid) { string instancePath = GetInstancePath(org, app, instanceOwnerId, instanceGuid); @@ -97,7 +109,7 @@ public static void PrepareInstance(string org, string app, int instanceOwnerId, File.Copy(preInstancePath, instancePath, true); string dataPath = GetDataDirectory(org, app, instanceOwnerId, instanceGuid); - + if (Directory.Exists(dataPath)) { foreach (string filePath in Directory.GetFiles(dataPath, "*.*", SearchOption.AllDirectories)) diff --git a/test/Altinn.App.Api.Tests/Data/authorization/claims/12345.json b/test/Altinn.App.Api.Tests/Data/authorization/claims/12345.json new file mode 100644 index 000000000..e3da13258 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/claims/12345.json @@ -0,0 +1,7 @@ +[ + { + "type": "some:extra:claim", + "value": "claimValue", + "valueType": "http://www.w3.org/2001/XMLSchema#string" + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/claims/1337.json b/test/Altinn.App.Api.Tests/Data/authorization/claims/1337.json new file mode 100644 index 000000000..e3da13258 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/claims/1337.json @@ -0,0 +1,7 @@ +[ + { + "type": "some:extra:claim", + "value": "claimValue", + "valueType": "http://www.w3.org/2001/XMLSchema#string" + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/partylist/1001.json b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1001.json new file mode 100644 index 000000000..d48be7f3c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1001.json @@ -0,0 +1,25 @@ +[ + { + "partyId": "510001", + "partyTypeName": 1, + "ssn": "01899699552", + "name": "Pengelens Partner", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500000", + "partyTypeName": 2, + "OrgNumber": "897069650", + "unitType": "AS", + "name": "DDG Fitness AS", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + ] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/partylist/1002.json b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1002.json new file mode 100644 index 000000000..928dce83a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1002.json @@ -0,0 +1,25 @@ +[ + { + "partyId": "510002", + "partyTypeName": 1, + "ssn": "17858296439", + "name": "Gjentagende Forelder", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500000", + "partyTypeName": 2, + "OrgNumber": "897069650", + "unitType": "AS", + "name": "DDG Fitness AS", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + ] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/partylist/1003.json b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1003.json new file mode 100644 index 000000000..d5ebed7e5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1003.json @@ -0,0 +1,13 @@ +[ + { + "partyId": "510003", + "partyTypeName": 1, + "ssn": "08829698278", + "name": "Rik Forelder", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + ] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/partylist/12345.json b/test/Altinn.App.Api.Tests/Data/authorization/partylist/12345.json new file mode 100644 index 000000000..da7122420 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/partylist/12345.json @@ -0,0 +1,13 @@ +[ + { + "partyId": "512345", + "partyTypeName": 1, + "ssn": "01017512345", + "name": "Ola Nordmann", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/partylist/1337.json b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1337.json new file mode 100644 index 000000000..eef98968c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/partylist/1337.json @@ -0,0 +1,124 @@ +[ + { + "partyId": "501337", + "partyTypeName": 1, + "ssn": "01038812345", + "unitType": null, + "name": "Sophie Salt", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500000", + "partyTypeName": 2, + "OrgNumber": "897069650", + "unitType": "AS", + "name": "DDG Fitness AS", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": [ + { + "partyId": "500001", + "partyTypeName": 2, + "OrgNumber": "897069651", + "unitType": "BEDR", + "name": "DDG Fitness Bergen", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500002", + "partyTypeName": 2, + "OrgNumber": "897069652", + "unitType": "BEDR", + "name": "DDG Fitness Oslo", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500003", + "partyTypeName": 2, + "OrgNumber": "897069653", + "unitType": "BEDR", + "name": "DDG Fitness Trondheim", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } + + ] + }, + { + "partyId": "500600", + "partyTypeName": 2, + "OrgNumber": "897069631", + "unitType": "AS", + "name": "EAS Health Consulting", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500700", + "partyTypeName": 2, + "OrgNumber": "950474084", + "unitType": "BRL", + "name": "Oslos Vakreste Borettslag", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500800", + "partyTypeName": 2, + "OrgNumber": "910457292", + "unitType": "AS", + "name": "EAS Health Consulting Sortland", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500801", + "partyTypeName": 2, + "OrgNumber": "910471120", + "unitType": "AS", + "name": "EAS Health Consulting Stokmarknes", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + }, + { + "partyId": "500802", + "partyTypeName": 2, + "OrgNumber": "910423495", + "unitType": "AS", + "name": "EAS Health Consulting Svolvær", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_119.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_119.json new file mode 100644 index 000000000..1d85c7d2b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_119.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T18:04:27.27", + "identifier": "appid-119", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "8f08210a-d792-48f5-9e27-0f029e41111e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:119", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_120.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_120.json new file mode 100644 index 000000000..80074640d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_120.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T19:07:46.143", + "identifier": "appid-120", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "a6574ca8-5836-46b0-91f0-8ebb0ff214cf", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:120", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_122.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_122.json new file mode 100644 index 000000000..3a3dd8529 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_122.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-06T09:36:15.443", + "identifier": "appid-122", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "da7650a5-d893-404a-9bfe-28d0904fe896", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:122", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_123.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_123.json new file mode 100644 index 000000000..350ae8793 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_123.json @@ -0,0 +1,65 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-06T09:41:15.817", + "identifier": "appid-123", + "isComplete": false, + "description": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "3cb27d7b-9e2c-475c-ba91-e1fc359bc717", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:123", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076", + "name": { + "en": "Accenture Norway", + "nb-no": "Accenture Norge", + "nn-no": "Accenture Norge" + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_124.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_124.json new file mode 100644 index 000000000..71e9ffb58 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_124.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-25T15:17:18.487", + "identifier": "appid-124", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "fb1bc117-333d-4dea-9400-6bd3e1ce07dd", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:124", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_125.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_125.json new file mode 100644 index 000000000..6098f6cf5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_125.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-25T15:20:40.17", + "identifier": "appid-125", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "c8e7b227-3d62-4682-81a2-f2865f0eb91f", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:125", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_126.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_126.json new file mode 100644 index 000000000..0710b4e31 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_126.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-13T11:10:52.203", + "identifier": "appid-126", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "4f9b0f81-74a6-4b76-8ce5-0e1fe153c872", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:126", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_127.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_127.json new file mode 100644 index 000000000..642932107 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_127.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-19T11:13:36.8", + "identifier": "appid-127", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "38da61a2-4cd8-4dac-9d6b-8205f91661c4", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:127", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_128.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_128.json new file mode 100644 index 000000000..e45c37f2a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_128.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-19T11:18:52.84", + "identifier": "appid-128", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "74f5dcb3-eee0-4a3b-9499-93fc2148a513", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:128", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_129.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_129.json new file mode 100644 index 000000000..0d3aeeb48 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_129.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-20T11:49:19.26", + "identifier": "appid-129", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "d0fc231c-aaa3-4e6a-bece-4e283e6e2f6f", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:129", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_130.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_130.json new file mode 100644 index 000000000..14de58b9f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_130.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-20T12:32:41.78", + "identifier": "appid-130", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "3fc8a856-d5b1-4861-ace0-6b3fd5ffd916", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:130", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_132.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_132.json new file mode 100644 index 000000000..b3a050708 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_132.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-28T23:38:06.337", + "identifier": "appid-132", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "923bdaaf-0a5e-4b63-908b-4a5ecf75e37b", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:132", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_133.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_133.json new file mode 100644 index 000000000..3eaa938d8 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_133.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-28T23:39:09.01", + "identifier": "appid-133", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "f7670967-0428-4b90-86a2-2ac637d0ec2a", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:133", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_134.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_134.json new file mode 100644 index 000000000..0b1de5d2f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_134.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-28T23:40:31.413", + "identifier": "appid-134", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "d812fd77-7a43-4332-a782-375b37aff1eb", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:134", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_136.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_136.json new file mode 100644 index 000000000..c0c70931d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_136.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-05-29T09:50:19.333", + "identifier": "appid-136", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "911f9e93-1541-429d-a8c6-a06f9a52d827", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:136", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "MAT", + "organization": "985399077" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_137.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_137.json new file mode 100644 index 000000000..d1d86b5b9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_137.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-06-05T14:05:14.83", + "identifier": "appid-137", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "938a1d17-5015-4a17-bd20-575bf5cd1102", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:137", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_138.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_138.json new file mode 100644 index 000000000..6596b80ca --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_138.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-06-17T12:46:18.437", + "identifier": "appid-138", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "b4c2ac55-fa7d-44d1-8c95-e64d47206a57", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:138", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SLK", + "organization": "960885406" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_139.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_139.json new file mode 100644 index 000000000..f2dabdc54 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_139.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Test-API for a demo", + "nb": "Test-API for demo", + "nn": "Test-API for ein demo" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-08-27T08:15:12.673", + "identifier": "appid-139", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "This service grants full access to a test API", + "nb": "Denne tjenesten gir full tilgang til et test-API", + "nn": "Denne tenesta gir full tilgang til eit test-API" + }, + "resourceReferences": [ + { + "reference": "0f184d85-afa3-4dcf-916b-5a8f85a12c95", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:ettellerannetscope.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:ettellerannetscope.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:139", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_142.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_142.json new file mode 100644 index 000000000..333d8aada --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_142.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-09-11T07:32:49.723", + "identifier": "appid-142", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "1f6952b3-84ef-4eec-b742-846d986691e3", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:142", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_144.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_144.json new file mode 100644 index 000000000..cd78ea3f7 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_144.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "JK's Security Level 3 Scheme", + "nb": "JK's Sikkerhetsnivå 3 Scheme", + "nn": "JK's Sikkringsnivå 3 Scheme" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-09-22T11:46:26.11", + "identifier": "appid-144", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Example of a DelegationScheme with security level 3 requirement", + "nb": "Eksempel på DelegationScheme med sikkerhetsnivå 3 krav", + "nn": "Eit døme på DelegationScheme med sikkringsnivå 3 krav" + }, + "resourceReferences": [ + { + "reference": "5f03a306-8a01-47b5-8219-8014843ce691", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/seclvl3.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/seclvl3.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:144", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_145.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_145.json new file mode 100644 index 000000000..c8b1b9329 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_145.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "JK's Security Level 2 Scheme", + "nb": "JK's Sikkerhetsnivå 2 Scheme", + "nn": "JK's Sikkringsnivå 2 Scheme" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-09-22T11:54:03.32", + "identifier": "appid-145", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Example of a DelegationScheme with security level 2 requirement", + "nb": "Eksempel på DelegationScheme med sikkerhetsnivå 2 krav", + "nn": "Eit døme på DelegationScheme med sikkringsnivå 2 krav" + }, + "resourceReferences": [ + { + "reference": "6685c771-459a-43ac-acb0-27a46d8f32d1", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/seclvl2.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/seclvl2.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:145", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_147.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_147.json new file mode 100644 index 000000000..487c6f9c2 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_147.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-09-29T12:32:40.68", + "identifier": "appid-147", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "08d6bac5-e581-4e34-b33e-1f3e2994159a", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:147", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_148.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_148.json new file mode 100644 index 000000000..549efffd1 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_148.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-09-30T10:57:40.96", + "identifier": "appid-148", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "6d91d828-5554-48f9-8875-3a5cb1cf562e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:148", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_150.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_150.json new file mode 100644 index 000000000..ce49e7beb --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_150.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-11-23T08:36:19.6", + "identifier": "appid-150", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "77b6ba03-b0e0-4b38-8477-0928f4a2131c", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:150", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_153.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_153.json new file mode 100644 index 000000000..20455dd90 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_153.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-12-16T11:19:44.693", + "identifier": "appid-153", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "4ee353be-b998-4e86-8366-99a9c8ece1bc", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:153", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_154.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_154.json new file mode 100644 index 000000000..b16078058 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_154.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-01-10T22:55:28.887", + "identifier": "appid-154", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "630031e8-8074-4e85-b39d-b5ec523354fa", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:154", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "NAV", + "organization": "889640782" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_155.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_155.json new file mode 100644 index 000000000..482e609eb --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_155.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-01-12T20:47:54.047", + "identifier": "appid-155", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "7a44f919-f4a6-4ac1-8caf-0d364e8f355f", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:155", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_164.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_164.json new file mode 100644 index 000000000..ad4eb5554 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_164.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-01-15T09:06:22.017", + "identifier": "appid-164", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "5fec884c-c232-456f-8b2f-9e54c0d1efb3", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:164", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_168.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_168.json new file mode 100644 index 000000000..09088b84e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_168.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-01-28T18:05:06.683", + "identifier": "appid-168", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "30168308-9f93-460f-868a-0edbb921ea8f", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:168", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_178.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_178.json new file mode 100644 index 000000000..9cafa317d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_178.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-15T19:51:35.823", + "identifier": "appid-178", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "04e68c88-92f4-42a1-a464-a97f41d7a58f", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:178", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_179.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_179.json new file mode 100644 index 000000000..49dd3cf11 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_179.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-15T19:54:20.687", + "identifier": "appid-179", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "692cc835-60b0-49d7-b10d-2f9fa3c07eec", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:179", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_180.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_180.json new file mode 100644 index 000000000..5915865b3 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_180.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-15T19:57:22.347", + "identifier": "appid-180", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "da9b46e2-479f-44c1-8423-01c3a0fab56c", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:180", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_181.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_181.json new file mode 100644 index 000000000..49c8b0224 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_181.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-15T19:57:28.673", + "identifier": "appid-181", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "73085841-e264-4257-bc0c-061c5707d54b", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:181", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_182.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_182.json new file mode 100644 index 000000000..6e2a47cca --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_182.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Regression", + "nb": "Regression", + "nn": "Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-15T19:57:42.953", + "identifier": "appid-182", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes", + "nb": "Gir anledning til å teste maskinporten", + "nn": "Gjer høve til å teste maskinporten" + }, + "resourceReferences": [ + { + "reference": "c6086d53-6d60-41b3-9f67-32a6347222e2", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:maskinporten/delegationschemes.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:182", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_184.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_184.json new file mode 100644 index 000000000..c1e0a94e9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_184.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-16T16:01:27.67", + "identifier": "appid-184", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "3210f4eb-2d76-4666-9574-664c0fd985f5", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:184", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_185.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_185.json new file mode 100644 index 000000000..a8139aa6a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_185.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-02-16T16:02:03.783", + "identifier": "appid-185", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "165a2af4-a9e3-4d94-a5f2-65ec749af10e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:185", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_191.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_191.json new file mode 100644 index 000000000..2adff8082 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_191.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-09T14:11:22.853", + "identifier": "appid-191", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "86a49da7-9d41-42ba-9ab8-0ded1f6b6b34", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:191", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SVV", + "organization": "971032081" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_192.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_192.json new file mode 100644 index 000000000..4918f9cb6 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_192.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-09T14:11:34.503", + "identifier": "appid-192", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "cc8878d7-21d2-43af-ae4a-27ffcd0f06e4", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:192", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SVV", + "organization": "971032081" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_193.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_193.json new file mode 100644 index 000000000..057ff5b11 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_193.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-09T20:12:53.51", + "identifier": "appid-193", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "47a69357-3028-48bc-abdf-1fd4857446bb", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:193", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_196.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_196.json new file mode 100644 index 000000000..b79f49601 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_196.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:35.29", + "identifier": "appid-196", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "b6bdc87c-e124-437b-9858-35f27171a24a", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:196", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_197.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_197.json new file mode 100644 index 000000000..dc409fbe5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_197.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:39.577", + "identifier": "appid-197", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "09ef4c47-0493-49ba-ba69-7028df31134c", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:197", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_198.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_198.json new file mode 100644 index 000000000..ee625ae16 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_198.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:43.623", + "identifier": "appid-198", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "98e7d274-3b64-4d10-b7d4-2b6e27026350", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:198", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_199.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_199.json new file mode 100644 index 000000000..a154221dc --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_199.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:47.017", + "identifier": "appid-199", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "6ac1efeb-0ae7-4e3d-aafd-5bfe2184e348", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:199", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_200.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_200.json new file mode 100644 index 000000000..49d3a9717 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_200.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:49.973", + "identifier": "appid-200", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "0d552f1c-dc41-43a6-9f76-4650434ed5e1", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:200", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_201.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_201.json new file mode 100644 index 000000000..6e4fe8038 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_201.json @@ -0,0 +1,41 @@ +{ + "title": { + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:54.017", + "identifier": "appid-201", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "45164e1d-147b-4c51-8446-19d9233a0c27", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:201", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_202.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_202.json new file mode 100644 index 000000000..a95c85c38 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_202.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T13:59:57.35", + "identifier": "appid-202", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "d60d98dd-830e-4554-b2d4-b9b76696f770", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:202", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_203.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_203.json new file mode 100644 index 000000000..596445af3 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_203.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T14:00:00.53", + "identifier": "appid-203", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "eb7a322c-98f5-4d91-8720-baf4b7a61dcc", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:203", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_204.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_204.json new file mode 100644 index 000000000..caa1fc95a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_204.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-03-19T14:00:04.45", + "identifier": "appid-204", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "18ab3b9a-a0c2-408c-b9fa-21838330c2fa", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:204", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_205.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_205.json new file mode 100644 index 000000000..89127804d --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_205.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-05-18T13:47:29.147", + "identifier": "appid-205", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "92e7f534-b126-415c-8a65-d7a8d8ff70a5", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:205", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SVV", + "organization": "971032081" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_206.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_206.json new file mode 100644 index 000000000..fd5fec1e9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_206.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-10T09:46:01.027", + "identifier": "appid-206", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "72f5b1bd-3e0e-49d0-8133-236cb8b628e5", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:206", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_207.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_207.json new file mode 100644 index 000000000..a6a265f1c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_207.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-10T09:47:12.093", + "identifier": "appid-207", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "71cee800-9b2c-4326-b336-7921223f9ca3", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:207", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_208.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_208.json new file mode 100644 index 000000000..368e9e320 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_208.json @@ -0,0 +1,54 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-10T09:47:54.12", + "identifier": "appid-208", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "fd9a3748-2c56-49dd-9860-64de0258ada3", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:208", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_209.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_209.json new file mode 100644 index 000000000..303ab013b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_209.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-10T09:48:49.723", + "identifier": "appid-209", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "f011cd46-d7f3-4037-975c-5bdcd166295e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:209", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_210.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_210.json new file mode 100644 index 000000000..3969827d9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_210.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-10T10:01:35.137", + "identifier": "appid-210", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "55d73b7c-6040-41ff-b0dc-877692ba0ea6", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:210", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_211.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_211.json new file mode 100644 index 000000000..3905088b1 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_211.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-12T13:27:33.327", + "identifier": "appid-211", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "43a73290-3193-49be-89f5-2b309e373442", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:211", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_212.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_212.json new file mode 100644 index 000000000..df15544ee --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_212.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-08-18T14:22:38", + "identifier": "appid-212", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "5295d5bd-c4c4-43c4-a55e-7c15ecde1ce7", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:212", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_213.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_213.json new file mode 100644 index 000000000..2179c75b6 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_213.json @@ -0,0 +1,52 @@ +{ + "title": { + "nb": "Bjørn 1" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-09-03T12:30:33.887", + "identifier": "appid-213", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "nb": "Bla bla bla bla bla" + }, + "resourceReferences": [ + { + "reference": "8931d72d-0bab-4c9f-a260-ce37810d6f16", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:213", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_214.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_214.json new file mode 100644 index 000000000..56a4a6261 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_214.json @@ -0,0 +1,52 @@ +{ + "title": { + "nb": "Bjørn 1" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-09-28T13:55:27.403", + "identifier": "appid-214", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "nb": "Bla bla bla bla bla" + }, + "resourceReferences": [ + { + "reference": "61aeb251-1e1c-4dcd-b33a-6e63e30b0e6f", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:214", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_215.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_215.json new file mode 100644 index 000000000..6f1eb9e5f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_215.json @@ -0,0 +1,52 @@ +{ + "title": { + "nb": "Automation Test Delegation Scheme Requires Level 3" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-10-27T16:11:59.113", + "identifier": "appid-215", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "nb": "Bla bla bla bla bla" + }, + "resourceReferences": [ + { + "reference": "89058b75-8ddb-4c69-8e5c-158b87f7e6cc", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:215", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "NAV", + "organization": "889640782" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_216.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_216.json new file mode 100644 index 000000000..70cec5881 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_216.json @@ -0,0 +1,52 @@ +{ + "title": { + "nb": "Automation Test Delegation Scheme Requires Level 3" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-10-27T16:12:00.827", + "identifier": "appid-216", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "nb": "Bla bla bla bla bla" + }, + "resourceReferences": [ + { + "reference": "9fd1e158-2dad-4e56-bf7f-ab2e69b73a0a", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.read", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:bjorn/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:216", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "NAV", + "organization": "889640782" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_217.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_217.json new file mode 100644 index 000000000..2ca4d15b2 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_217.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-12-13T12:12:34.827", + "identifier": "appid-217", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "ea6e52be-b4d6-44d3-a272-f955b84b34e6", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:217", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_218.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_218.json new file mode 100644 index 000000000..bc5669531 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_218.json @@ -0,0 +1,56 @@ +{ + "title": { + "en": "Automation Regression", + "nb": "Automation Regression", + "nn": "Automation Regression" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2021-12-13T12:12:49.563", + "identifier": "appid-218", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Allows you to test maskinporten changes as part of automation testing", + "nb": "Gir anledning til a teste maskinporten som en del av automatiserte tester", + "nn": "Gjer hove til a teste maskinporten som en del av automatiserte tester" + }, + "resourceReferences": [ + { + "reference": "ed752be2-e80b-4cc5-83e5-263dbc1cf60d", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.write", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "altinn:test/theworld.admin", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:218", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_219.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_219.json new file mode 100644 index 000000000..fc2f147b7 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_219.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2022-03-01T13:28:17.143", + "identifier": "appid-219", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "5b16379f-f999-4805-9a67-2c32ce304ffa", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:219", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DFO", + "organization": "986252932" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_220.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_220.json new file mode 100644 index 000000000..b023e8a2e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_220.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2022-04-01T15:29:03.213", + "identifier": "appid-220", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "122cea91-6b64-4ea4-b460-3ab597ffe15d", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:220", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "NHN", + "organization": "994598759" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_221.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_221.json new file mode 100644 index 000000000..aaf28ddb8 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_221.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2022-04-20T13:31:33.433", + "identifier": "appid-221", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "fae69028-5497-432f-9a32-d7821b487fff", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:221", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "MAT", + "organization": "985399077" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_222.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_222.json new file mode 100644 index 000000000..4aac44a02 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_222.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2022-06-08T09:45:50.453", + "identifier": "appid-222", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "95c94595-c026-4b7b-880a-7c03d9736ceb", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:222", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "SKD", + "organization": "974761076" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_223.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_223.json new file mode 100644 index 000000000..b0221008f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_223.json @@ -0,0 +1,38 @@ +{ + "title": {}, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2022-06-13T13:41:02.943", + "identifier": "appid-223", + "isComplete": false, + "description": null, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": {}, + "resourceReferences": [ + { + "reference": "2d04277f-f4a9-498f-b317-8bfacb0d19e9", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:223", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "DIGDIR", + "organization": "991825827" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_400.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_400.json new file mode 100644 index 000000000..f1b3f7921 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_400.json @@ -0,0 +1,60 @@ +{ + "title": { + "en": "Humbug Registry", + "nb": "Tulleregisteret", + "nn": "Tulleregisteret" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T18:04:27.27", + "identifier": "appid-400", + "isComplete": false, + "description": { + "en": "Humbug Registry", + "nb": "Tulleregisteret", + "nn": "Tulleregisteret" + }, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Gives access to silly things.", + "nb": "Gir tilgang til tullete ting.", + "nn": "Gir tilgang til tullete ting." + }, + "resourceReferences": [ + { + "reference": "8f08210a-d792-48f5-9e27-0f029e41111e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:paa/v1/luring", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:400", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "PAA", + "organization": "985399077", + "name": { + "en": "DEPARTMENT OF HUMBUG", + "nb": "PÅFUNNSETATEN", + "nn": "PÅFUNNSETATEN" + } + } +} diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_401.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_401.json new file mode 100644 index 000000000..e46e160a8 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_401.json @@ -0,0 +1,65 @@ +{ + "title": { + "en": "The Flowergarden", + "nb": "Blomsterhagen", + "nn": "Blomsterhagen" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T18:04:27.27", + "identifier": "appid-401", + "isComplete": false, + "description": { + "en": "The Flowergarden", + "nb": "Blomsterhagen", + "nn": "Blomsterhagen" + }, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Gives access to beautiful flowers.", + "nb": "Gir tilgang til vakre blomster.", + "nn": "Gir tilgang til vakre blomster." + }, + "resourceReferences": [ + { + "reference": "8f08210a-d792-48f5-9e27-0f029e41111e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:aareg/v1/arbeidsforhold/otp", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:bfinn/v1/hagen", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:401", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "BFINN", + "organization": "994598759", + "name": { + "en": "BLOMSTERFINN", + "nb": "BLOMSTERFINN", + "nn": "BLOMSTERFINN" + } + } +} diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_402.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_402.json new file mode 100644 index 000000000..bb1ce84ed --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_402.json @@ -0,0 +1,65 @@ +{ + "title": { + "en": "The Magic Closet", + "nb": "Det magiske klesskapet", + "nn": "Det magiske klesskapet" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T18:04:27.27", + "identifier": "appid-402", + "isComplete": false, + "description": { + "en": "The Magic Closet", + "nb": "Det magiske klesskapet", + "nn": "Det magiske klesskapet" + }, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Gives access to Narnia.", + "nb": "Gir tilgang til Narnia.", + "nn": "Gir tilgang til Narnia." + }, + "resourceReferences": [ + { + "reference": "8f08210a-d792-48f5-9e27-0f029e41111e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:nrna/v1/kaape/otp", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:nrna/huset/dettommerommet", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:402", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "NRNA", + "organization": "971032081", + "name": { + "en": "NARNIA", + "nb": "NARNIA", + "nn": "NARNIA" + } + } +} diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_403.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_403.json new file mode 100644 index 000000000..17785d24a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_403.json @@ -0,0 +1,60 @@ +{ + "title": { + "en": "The Shortcut", + "nb": "Snarveien", + "nn": "Snarvegen" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T18:04:27.27", + "identifier": "appid-403", + "isComplete": false, + "description": { + "en": "The Shortcut", + "nb": "Snarveien", + "nn": "Snarvegen" + }, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Gives access to shortcuts.", + "nb": "Gir tilgang til snarveier.", + "nn": "Gir tilgang til snarveger." + }, + "resourceReferences": [ + { + "reference": "8f08210a-d792-48f5-9e27-0f029e41111e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:paa/v1/snartenkt", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:400", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "PAA", + "organization": "985399077", + "name": { + "en": "DEPARTMENT OF HUMBUG", + "nb": "PÅFUNNSETATEN", + "nn": "PÅFUNNSETATEN" + } + } +} diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_43.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_43.json new file mode 100644 index 000000000..893c24894 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/Appid_43.json @@ -0,0 +1,60 @@ +{ + "title": { + "en": "Aa-registeret OTP API", + "nb": "Aa-registeret OTP API", + "nn": "Aa-registeret OTP API" + }, + "sector": null, + "status": null, + "validTo": "9999-12-31T23:59:59.997", + "homepage": null, + "isPartOf": null, + "keywords": null, + "validFrom": "2020-03-04T18:04:27.27", + "identifier": "appid-43", + "isComplete": false, + "description": { + "en": "Aa-registeret OTP API", + "nb": "Aa-registeret OTP API", + "nn": "Aa-registeret OTP API" + }, + "resourceType": "MaskinportenSchema", + "thematicArea": null, + "isPublicService": true, + "rightDescription": { + "en": "Gives the pension cooperation access to employee relationships registered to a legal entity.", + "nb": "Gir pensjonsinnretningen tilgang til arbeidsforhold registrert på en opplysningspliktig.", + "nn": "Gir pensjonsinnretninga tilgang på dei arbeidforholda som er registrert på ein opplysingspliktig." + }, + "resourceReferences": [ + { + "reference": "8f08210a-d792-48f5-9e27-0f029e41111e", + "referenceType": "DelegationSchemeId", + "referenceSource": "Altinn2" + }, + { + "reference": "nav:aareg/v1/arbeidsforhold/otp", + "referenceType": "MaskinportenScope", + "referenceSource": "Altinn2" + }, + { + "reference": "AppId:43", + "referenceType": "ServiceCode", + "referenceSource": "Altinn2" + }, + { + "reference": "1", + "referenceType": "ServiceEditionCode", + "referenceSource": "Altinn2" + } + ], + "hasCompetentAuthority": { + "orgcode": "NAV", + "organization": "974761076", + "name": { + "en": "ARBEIDS- OG VELFERDSETATEN", + "nb": "ARBEIDS- OG VELFERDSETATEN", + "nn": "ARBEIDS- OG VELFERDSETATEN" + } + } +} diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/altinn_access_management.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/altinn_access_management.json new file mode 100644 index 000000000..8cefddb93 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/altinn_access_management.json @@ -0,0 +1,26 @@ +{ + "identifier": "altinn_access_management", + "description": { + "nb": "Funksjonalitet for � tilgangsstyring I Altinn" + }, + "title": { + "nb": "Tilgangsstyring Altinn" + }, + "hasCompetentAuthority": { + "organization": "991825827", + "orgcode": "digdir" + }, + "contactpoint": [ + { + "phone": "1231324", + "email": "online@digdir.no" + } + ], + "isPartOf": "altiinnportal", + "hasPart": "delegationrequests", + "homepage": "www.altinn.no", + "status": "Completed", + "thematicArea": "http://publications.europa.eu/resource/authority/eurovoc/2468", + "type": [], + "sector": [] +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/nav_tiltakAvtaleOmArbeidstrening.json b/test/Altinn.App.Api.Tests/Data/authorization/resources/nav_tiltakAvtaleOmArbeidstrening.json new file mode 100644 index 000000000..f8c744021 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/nav_tiltakAvtaleOmArbeidstrening.json @@ -0,0 +1,46 @@ +{ + "identifier": "nav_tiltakAvtaleOmArbeidstrening", + "title": { + "nb": "Avtale om arbeidstrening", + "en": "Agreement on work training" + }, + "description": { + "nb-NO": "Avtalen skal bidra til at personer med behov for bistand fra NAV skal f eller beholde en jobb, samtidig som den sttter arbeidsgivere som inkluderer deg som har nedsatt arbeidsevne. Avtalen inngs mellom arbeidsgiver, arbeidssker og NAV.", + "EN": "The agreement will help people who need assistance from NAV to get or keep a job, while supporting employers who include those who have reduced working capacity. The agreement is entered into between the employer, the jobseeker and NAV." + }, + "rightDescription": { + "nb-NO": "Med denne fullmakten kan man inng en avtale om arbeidstreing med NAV og kommune.", + "EN": "With this authorisation, you can enter into an agreement on work training with NAV and the municipality" + }, + "hasCompetentAuthority": { + "organization": "889640782", + "orgcode": "NAV" + }, + "contactpoint": [ + { + "phone": "55 55 33 36 ", + "email": "postmottak@nav.no" + } + ], + "homepage": "https://www.nav.no/", + "status": "Active", + "validFrom": "2019-05-08T14:00:00", + "validTo": "2049-05-08T14:00:00", + "isPartOf": "nav_RapporteringOmArbeidstrening", + "isPublicService": true, + "usedBy": [ "urn:citizen", "urn:enterprise", "urn:selfidentifeduser" ], + "thematicArea": "http://publications.europa.eu/resource/authority/eurovoc/2468", + "type": [], + "sector": [], + "keywords": [ + { + "word" : "Ddsbo", + "language" : "nb" + } + ], + "resourceReference": { + "referenceSource": "Altinn2", + "referenceType": "ServiceCodeVersion", + "reference": "5332/2" + } +} diff --git a/test/Altinn.App.Api.Tests/Data/authorization/resources/policies/altinn_maskinporten_scope_delegation.xml b/test/Altinn.App.Api.Tests/Data/authorization/resources/policies/altinn_maskinporten_scope_delegation.xml new file mode 100644 index 000000000..f86c15c26 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/resources/policies/altinn_maskinporten_scope_delegation.xml @@ -0,0 +1,52 @@ + + + + + Eksempel på samleregel som spesifiserer at både REGNA og DAGL m/sikkerhetsnivå; 2, for ressursen; SKD/TaxReport får tilgang til operasjonene; Read, Write og Instantiate for Event; Tasks; FormFilling og Signing + + + + + regna + + + + + + dagl + + + + + + + + altinn_maskinporten_scope_delegation + + + + + + + + read + + + + + + write + + + + + + + + + + 2 + + + + diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_500000/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_500000/roles.json new file mode 100644 index 000000000..ee27c7b99 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_500000/roles.json @@ -0,0 +1,14 @@ +[ + { + "Type": "altinn", + "value": "A0239" + }, + { + "Type": "altinn", + "value": "A0240" + }, + { + "Type": "altinn", + "value": "A0241" + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_3/party_1003/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_510001/roles.json similarity index 66% rename from test/Altinn.App.Api.Tests/Data/authorization/roles/user_3/party_1003/roles.json rename to test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_510001/roles.json index c54bcd426..ebdd9bba3 100644 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_3/party_1003/roles.json +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1001/party_510001/roles.json @@ -6,5 +6,9 @@ { "Type": "altinn", "value": "dagl" + }, + { + "Type": "altinn", + "value": "priv" } -] +] \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1000/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_500000/roles.json similarity index 60% rename from test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1000/roles.json rename to test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_500000/roles.json index eb5c181bd..a416c98b3 100644 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1000/roles.json +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_500000/roles.json @@ -1,11 +1,10 @@ [ { "Type": "altinn", - "value": "REGNA" + "value": "A0237" }, { "Type": "altinn", - "value": "DAGL" + "value": "A0238" } ] - diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_510002/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_510002/roles.json new file mode 100644 index 000000000..e660cbeb0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1002/party_510002/roles.json @@ -0,0 +1,16 @@ +[ + { + "Type": "altinn", + "value": "regna" + }, + { + "Type": "altinn", + "value": "dagl" + }, + { + "Type": "altinn", + "value": "priv" + } + ] + + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1003/party_510003/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1003/party_510003/roles.json new file mode 100644 index 000000000..e660cbeb0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1003/party_510003/roles.json @@ -0,0 +1,16 @@ +[ + { + "Type": "altinn", + "value": "regna" + }, + { + "Type": "altinn", + "value": "dagl" + }, + { + "Type": "altinn", + "value": "priv" + } + ] + + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/party_12345/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/party_512345/roles.json similarity index 67% rename from test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/party_12345/roles.json rename to test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/party_512345/roles.json index c7f805fc1..d8d132a58 100644 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/party_12345/roles.json +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_12345/party_512345/roles.json @@ -6,6 +6,10 @@ { "Type": "altinn", "value": "dagl" + }, + { + "Type": "altinn", + "value": "priv" } ] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_1404/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_1404/roles.json deleted file mode 100644 index ebec433ce..000000000 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_1404/roles.json +++ /dev/null @@ -1,67 +0,0 @@ -[ - { - "Type": "altinn", - "value": "PRIV" - }, - { - "Type": "altinn", - "value": "UTINN" - }, - { - "Type": "altinn", - "value": "LOPER" - } - , - { - "Type": "altinn", - "value": "ADMAI" - }, - { - "Type": "altinn", - "value": "PRIUT" - }, - { - "Type": "altinn", - "value": "REGNA" - }, - { - "Type": "altinn", - "value": "SISKD" - }, - { - "Type": "altinn", - "value": "UILUF" - }, - { - "Type": "altinn", - "value": "UTOMR" - }, - { - "Type": "altinn", - "value": "PAVAD" - }, - { - "Type": "altinn", - "value": "KOMAB" - }, - { - "Type": "altinn", - "value": "BOADM" - }, - { - "Type": "altinn", - "value": "A0212" - }, - { - "Type": "altinn", - "value": "A0236" - }, - { - "Type": "altinn", - "value": "A0278" - }, - { - "Type": "altinn", - "value": "A0282" - } -] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500000/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500000/roles.json index 22fefadd2..d68902de9 100644 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500000/roles.json +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500000/roles.json @@ -1,7 +1,7 @@ [ - { - "Type": "altinn", - "value": "DAGL" + { + "Type": "altinn", + "value": "DAGL" }, { "Type": "altinn", @@ -11,9 +11,9 @@ "Type": "altinn", "value": "ADMAI" }, - { - "Type": "altinn", - "value": "REGNA" + { + "Type": "altinn", + "value": "REGNA" }, { "Type": "altinn", diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500800/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500800/roles.json new file mode 100644 index 000000000..b772f6fc0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500800/roles.json @@ -0,0 +1,18 @@ +[ + { + "Type": "altinn", + "value": "MEDL" + }, + { + "Type": "altinn", + "value": "REGNA" + }, + { + "Type": "altinn", + "value": "UTINN" + }, + { + "Type": "altinn", + "value": "UTOMR" + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500801/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500801/roles.json new file mode 100644 index 000000000..b772f6fc0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500801/roles.json @@ -0,0 +1,18 @@ +[ + { + "Type": "altinn", + "value": "MEDL" + }, + { + "Type": "altinn", + "value": "REGNA" + }, + { + "Type": "altinn", + "value": "UTINN" + }, + { + "Type": "altinn", + "value": "UTOMR" + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500802/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500802/roles.json new file mode 100644 index 000000000..b772f6fc0 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_500802/roles.json @@ -0,0 +1,18 @@ +[ + { + "Type": "altinn", + "value": "MEDL" + }, + { + "Type": "altinn", + "value": "REGNA" + }, + { + "Type": "altinn", + "value": "UTINN" + }, + { + "Type": "altinn", + "value": "UTOMR" + } +] diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_1337/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_501337/roles.json similarity index 100% rename from test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_1337/roles.json rename to test/Altinn.App.Api.Tests/Data/authorization/roles/User_1337/party_501337/roles.json diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1002/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1002/roles.json deleted file mode 100644 index c7f805fc1..000000000 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_1/party_1002/roles.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "Type": "altinn", - "value": "regna" - }, - { - "Type": "altinn", - "value": "dagl" - } -] - diff --git a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_2/party_1000/roles.json b/test/Altinn.App.Api.Tests/Data/authorization/roles/user_2/party_1000/roles.json deleted file mode 100644 index f50db8736..000000000 --- a/test/Altinn.App.Api.Tests/Data/authorization/roles/user_2/party_1000/roles.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "Type": "altinn", - "value": "revai" - } -] - diff --git a/test/Altinn.App.Api.Tests/Mocks/AltinnPartyClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/AltinnPartyClientMock.cs new file mode 100644 index 000000000..3d8db7695 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/AltinnPartyClientMock.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Core.Internal.Registers; +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Api.Tests.Mocks; + +public class AltinnPartyClientMock : IAltinnPartyClient +{ + private readonly string _partyFolder = TestData.GetAltinnProfilePath(); + + public async Task GetParty(int partyId) + { + var file = Path.Join(_partyFolder, $"{partyId}.json"); + await using var fileHandle = File.OpenRead(file); // Throws exception if missing (helps with debugging tests) + return await JsonSerializer.DeserializeAsync(fileHandle, new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + Converters = { new JsonStringEnumConverter() } + }); + } + + public async Task LookupParty(PartyLookup partyLookup) + { + var files = Directory.GetFiles(_partyFolder, "*.json"); + foreach (var file in files) + { + var fileHandle = File.OpenRead(file); + var party = (await JsonSerializer.DeserializeAsync(fileHandle))!; + if (partyLookup.OrgNo != null && party.OrgNumber == partyLookup.OrgNo) + { + return party; + } + + if (partyLookup.Ssn != null && party.SSN == partyLookup.Ssn) + { + return party; + } + } + + // Current implementation throws PlatformException if party is not found. Not sure what the correct behaviour for tests is. + throw new Exception( + $"Could not find party with orgNo {partyLookup.OrgNo} or ssn {partyLookup.Ssn} in {_partyFolder}"); + } +} diff --git a/test/Altinn.App.Api.Tests/Mocks/AppModelMock.cs b/test/Altinn.App.Api.Tests/Mocks/AppModelMock.cs new file mode 100644 index 000000000..54fcce581 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/AppModelMock.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using Altinn.App.Core.Internal.AppModel; + +namespace Altinn.App.Api.Tests.Mocks; + +public class AppModelMock: IAppModel +{ + public object Create(string classRef) + { + return Activator.CreateInstance(GetModelType(classRef))!; + } + + public Type GetModelType(string classRef) + { + // The default implementations uses the executing assembly, but this does not work in the test project. + return Assembly.GetAssembly(typeof(AppModelMock))!.GetType(classRef, true)!; + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Mocks/Event/InstanceEventClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/Event/InstanceEventClientMock.cs new file mode 100644 index 000000000..920d80a2e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/Event/InstanceEventClientMock.cs @@ -0,0 +1,18 @@ +using Altinn.App.Core.Internal.Instances; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Api.Tests.Mocks.Event; + +public class InstanceEventClientMock : IInstanceEventClient +{ + public Task SaveInstanceEvent(object dataToSerialize, string org, string app) + { + return Task.FromResult(Guid.NewGuid().ToString()); + } + + public Task> GetInstanceEvents(string instanceId, string instanceOwnerPartyId, string org, string app, string[] eventTypes, + string from, string to) + { + throw new NotImplementedException(); + } +} diff --git a/test/Altinn.App.Api.Tests/Mocks/ProfileClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/ProfileClientMock.cs new file mode 100644 index 000000000..583608eb2 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/ProfileClientMock.cs @@ -0,0 +1,20 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Core.Internal.Profile; +using Altinn.Platform.Profile.Models; + +namespace Altinn.App.Api.Tests.Mocks; + +public class ProfileClientMock : IProfileClient +{ + public async Task GetUserProfile(int userId) + { + var folder = TestData.GetRegisterProfilePath(); + var file = Path.Join(folder, $"{userId}.json"); + return (await JsonSerializer.DeserializeAsync(File.OpenRead(file), new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + Converters = { new JsonStringEnumConverter() } + }))!; + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs index 520fc5a40..a43627d5d 100644 --- a/test/Altinn.App.Api.Tests/Program.cs +++ b/test/Altinn.App.Api.Tests/Program.cs @@ -5,10 +5,13 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; using AltinnCore.Authentication.JwtCookie; using App.IntegrationTests.Mocks.Services; using Microsoft.AspNetCore.Builder; @@ -53,6 +56,10 @@ void ConfigureMockServices(IServiceCollection services, ConfigurationManager con services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } void Configure() From 7bc0886531b1a066a63e305856562447fbe59b35 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Sat, 25 Nov 2023 23:45:26 +0100 Subject: [PATCH 31/46] Update microsoft dependencies for net8.0 (#365) Source link is no longer required: https://github.com/dotnet/sourcelink/releases/tag/8.0.0 > Source Link is now included in .NET SDK 8 and enabled by default. Projects that migrate to .NET SDK 8 do not need to reference Source Link packages explicitly via PackageReference anymore. --- src/Directory.Build.targets | 7 ------- test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj | 4 ++-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index dda4dc735..be3c00ec5 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,11 +1,4 @@ - - - all - runtime; build; native; contentfiles; analyzers - - - $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj index 9f797b19a..2da11ecef 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -14,8 +14,8 @@ - - + + From 81296a3c16126e8474108fc9e47a48e9441ce6a1 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Mon, 27 Nov 2023 08:58:31 +0100 Subject: [PATCH 32/46] V8 feature/304 support custom button (#346) * Make multi decision request for actions * Prepare code for useraction in task * add controller endpoint * trigger userdefined task actions * Add authorization check * Add some tests * controller tests for actions * added more tests for actionscontroller * Change how errors are returned * Add tests for MultiDecisionHelper and add xml comments * Add tests for AuthorizationClient * Fix some codesmells and add some more tests * Fix style error * Action Controller return 404 if no implementations found Fix comments from review * Apply suggestions from code review Co-authored-by: Ivar Nesje * Apply suggestion from code review Remove IUserActionService and rename UserActionFactory to UserActionService Add JsonPropertyName to classes returned through the ActionsController * Remove ValidationGroup from IUserAction * Apply suggestions from code review Co-authored-by: Ivar Nesje * Remove the need for NullUserAction --------- Co-authored-by: Ivar Nesje --- .../Controllers/ActionsController.cs | 129 ++++++ .../Controllers/ProcessController.cs | 29 +- .../Models/UserActionRequest.cs | 28 ++ .../Models/UserActionResponse.cs | 29 ++ .../Extensions/ServiceCollectionExtensions.cs | 3 +- .../Features/Action/NullUserAction.cs | 18 - .../Features/Action/SigningUserAction.cs | 10 +- ...rActionFactory.cs => UserActionService.cs} | 16 +- src/Altinn.App.Core/Features/IUserAction.cs | 2 +- .../Helpers/MultiDecisionHelper.cs | 185 ++++++++ .../Authorization/AuthorizationClient.cs | 25 +- .../Clients/Storage/ProcessClient.cs | 2 +- .../Internal/Auth/AuthorizationService.cs | 22 + .../Internal/Auth/IAuthorizationClient.cs | 12 + .../Internal/Auth/IAuthorizationService.cs | 12 + .../Internal/Exceptions/NotFoundException.cs | 15 + .../AltinnExtensionProperties/AltinnAction.cs | 54 +++ .../Process/Elements/AppProcessElementInfo.cs | 8 + .../Internal/Process/Elements/UserAction.cs | 31 ++ .../Internal/Process/ProcessEngine.cs | 14 +- .../Models/UserAction/ActionError.cs | 27 ++ .../Models/UserAction/FrontendAction.cs | 34 ++ .../Models/UserAction/UserActionContext.cs | 16 +- .../Models/UserAction/UserActionResult.cs | 76 ++++ .../Controllers/ActionsControllerTests.cs | 207 +++++++++ ...9-628e-4a6e-9efd-e4282068ef41.pretest.json | 39 ++ ...9-628e-4a6e-9efd-e4282068ef42.pretest.json | 31 ++ ...9-628e-4a6e-9efd-e4282068ef43.pretest.json | 26 ++ .../apps/tdd/task-action/appsettings.json | 37 ++ .../config/applicationmetadata.json | 53 +++ .../config/authorization/policy.xml | 401 ++++++++++++++++++ .../task-action/config/process/process.bpmn | 26 ++ .../task-action/config/texts/resource.nb.json | 33 ++ .../Mocks/AuthorizationMock.cs | 22 + .../Utils/PrincipalUtil.cs | 14 +- .../Altinn.App.Core.Tests.csproj | 5 +- .../Features/Action/SigningUserActionTests.cs | 2 +- .../Features/Action/UserActionFactoryTests.cs | 74 ---- .../Features/Action/UserActionServiceTests.cs | 75 ++++ .../Helpers/MultiDecisionHelperTests.cs | 265 ++++++++++++ .../all-actions-allowed.json | 230 ++++++++++ ...idecision-all-actions-endevent.golden.json | 126 ++++++ ...multidecision-all-actions-guid.golden.json | 126 ++++++ ...multidecision-all-actions-task.golden.json | 126 ++++++ .../one-action-denied.json | 188 ++++++++ .../Authorization/AuthorizationClientTests.cs | 132 ++++++ .../TestData/one-action-denied.json | 190 +++++++++ .../InstanceClientMetricsDecoratorTests.cs | 42 +- .../Auth/AuthorizationServiceTests.cs | 165 ++++--- .../Internal/Process/ProcessEngineTest.cs | 2 +- .../Internal/Process/ProcessNavigatorTests.cs | 15 +- .../Internal/Process/ProcessReaderTests.cs | 6 +- .../TestData/simple-gateway-default.bpmn | 1 + 53 files changed, 3220 insertions(+), 236 deletions(-) create mode 100644 src/Altinn.App.Api/Controllers/ActionsController.cs create mode 100644 src/Altinn.App.Api/Models/UserActionRequest.cs create mode 100644 src/Altinn.App.Api/Models/UserActionResponse.cs delete mode 100644 src/Altinn.App.Core/Features/Action/NullUserAction.cs rename src/Altinn.App.Core/Features/Action/{UserActionFactory.cs => UserActionService.cs} (59%) create mode 100644 src/Altinn.App.Core/Helpers/MultiDecisionHelper.cs create mode 100644 src/Altinn.App.Core/Internal/Exceptions/NotFoundException.cs create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/UserAction.cs create mode 100644 src/Altinn.App.Core/Models/UserAction/ActionError.cs create mode 100644 src/Altinn.App.Core/Models/UserAction/FrontendAction.cs create mode 100644 src/Altinn.App.Core/Models/UserAction/UserActionResult.cs create mode 100644 test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef41.pretest.json create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef42.pretest.json create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef43.pretest.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/appsettings.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/applicationmetadata.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/authorization/policy.xml create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/process/process.bpmn create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/texts/resource.nb.json delete mode 100644 test/Altinn.App.Core.Tests/Features/Action/UserActionFactoryTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs create mode 100644 test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs create mode 100644 test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/all-actions-allowed.json create mode 100644 test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-endevent.golden.json create mode 100644 test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-guid.golden.json create mode 100644 test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-task.golden.json create mode 100644 test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/one-action-denied.json create mode 100644 test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs create mode 100644 test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/TestData/one-action-denied.json diff --git a/src/Altinn.App.Api/Controllers/ActionsController.cs b/src/Altinn.App.Api/Controllers/ActionsController.cs new file mode 100644 index 000000000..be56f0128 --- /dev/null +++ b/src/Altinn.App.Api/Controllers/ActionsController.cs @@ -0,0 +1,129 @@ +#nullable enable +using Altinn.App.Api.Infrastructure.Filters; +using Altinn.App.Api.Models; +using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Internal.Exceptions; +using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.UserAction; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using IAuthorizationService = Altinn.App.Core.Internal.Auth.IAuthorizationService; + +namespace Altinn.App.Api.Controllers; + +/// +/// Controller that handles actions performed by users +/// +[AutoValidateAntiforgeryTokenIfAuthCookie] +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] +[Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/actions")] +public class ActionsController : ControllerBase +{ + private readonly IAuthorizationService _authorization; + private readonly IInstanceClient _instanceClient; + private readonly UserActionService _userActionService; + + /// + /// Create new instance of the class + /// + /// The authorization service + /// The instance client + /// The user action service + public ActionsController(IAuthorizationService authorization, IInstanceClient instanceClient, UserActionService userActionService) + { + _authorization = authorization; + _instanceClient = instanceClient; + _userActionService = userActionService; + } + + /// + /// Perform a task action on an instance + /// + /// unique identfier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that this the owner of the instance + /// unique id to identify the instance + /// user action request + /// + [HttpPost] + [Authorize] + [ProducesResponseType(typeof(UserActionResponse), 200)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(401)] + public async Task> Perform( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromBody] UserActionRequest actionRequest) + { + var action = actionRequest.Action; + if (action == null) + { + return new BadRequestObjectResult(new ProblemDetails() + { + Instance = instanceGuid.ToString(), + Status = 400, + Title = "Action is missing", + Detail = "Action is missing in the request" + }); + } + + var instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + + if (instance?.Process == null) + { + return Conflict($"Process is not started."); + } + + if (instance.Process.Ended.HasValue) + { + return Conflict($"Process is ended."); + } + + var 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); + if (!authorized) + { + return Forbid(); + } + + UserActionContext userActionContext = new UserActionContext(instance, userId.Value, actionRequest.ButtonId, actionRequest.Metadata); + var actionHandler = _userActionService.GetActionHandler(action); + if (actionHandler == null) + { + return new NotFoundObjectResult(new UserActionResponse() + { + Error = new ActionError() + { + Code = "ActionNotFound", + Message = $"Action handler with id {action} not found", + } + }); + } + + var result = await actionHandler.HandleAction(userActionContext); + + if (!result.Success) + { + return new BadRequestObjectResult(new UserActionResponse() + { + FrontendActions = result.FrontendActions, + Error = result.Error + }); + } + + return new OkObjectResult(new UserActionResponse() + { + FrontendActions = result.FrontendActions, + UpdatedDataModels = result.UpdatedDataModels + }); + } +} diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index f3bbe87fe..d853bbf10 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -81,7 +81,7 @@ public async Task> GetProcessState( try { Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); - AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, instance.Process); + AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process); return Ok(appProcessState); } @@ -136,7 +136,7 @@ public async Task> StartProcess( return Conflict(result.ErrorMessage); } - AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, result.ProcessStateChange?.NewProcessState); + AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, result.ProcessStateChange?.NewProcessState); return Ok(appProcessState); } catch (PlatformHttpException e) @@ -302,7 +302,7 @@ public async Task> NextElement( } } - AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, result.ProcessStateChange?.NewProcessState); + AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, result.ProcessStateChange?.NewProcessState); return Ok(appProcessState); } @@ -426,7 +426,7 @@ public async Task> CompleteProcess( return StatusCode(500, $"More than {counter} iterations detected in process. Possible loop. Fix app process definition!"); } - AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, instance.Process); + AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process); return Ok(appProcessState); } @@ -456,7 +456,7 @@ public async Task GetProcessHistory( } } - private async Task ConvertAndAuthorizeActions(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, ProcessState? processState) + private async Task ConvertAndAuthorizeActions(Instance instance, ProcessState? processState) { AppProcessState appProcessState = new AppProcessState(processState); if (appProcessState.CurrentTask?.ElementId != null) @@ -465,13 +465,13 @@ private async Task ConvertAndAuthorizeActions(string org, strin if (flowElement is ProcessTask processTask) { appProcessState.CurrentTask.Actions = new Dictionary(); - foreach (AltinnAction action in processTask.ExtensionElements?.TaskExtension?.AltinnActions ?? new List()) - { - appProcessState.CurrentTask.Actions.Add(action.Value, await AuthorizeAction(action.Value, org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id)); - } - - appProcessState.CurrentTask.HasWriteAccess = await AuthorizeAction("write", org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id); - appProcessState.CurrentTask.HasReadAccess = await AuthorizeAction("read", org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id); + List actions = new List() { new("read"), new("write") }; + actions.AddRange(processTask.ExtensionElements?.TaskExtension?.AltinnActions ?? new List()); + var authDecisions = await AuthorizeActions(actions, instance); + appProcessState.CurrentTask.Actions = authDecisions.Where(a => a.ActionType == ActionType.ProcessAction).ToDictionary(a => a.Id, a => a.Authorized); + appProcessState.CurrentTask.HasReadAccess = authDecisions.Single(a => a.Id == "read").Authorized; + appProcessState.CurrentTask.HasWriteAccess = authDecisions.Single(a => a.Id == "write").Authorized; + appProcessState.CurrentTask.UserActions = authDecisions; } } @@ -499,6 +499,11 @@ private async Task AuthorizeAction(string action, string org, string app, return await _authorization.AuthorizeAction(new AppIdentifier(org, app), new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), HttpContext.User, action, taskId); } + private async Task> AuthorizeActions(List actions, Instance instance) + { + return await _authorization.AuthorizeActions(instance, HttpContext.User, actions); + } + private static string EnsureActionNotTaskType(string actionOrTaskType) { switch (actionOrTaskType) diff --git a/src/Altinn.App.Api/Models/UserActionRequest.cs b/src/Altinn.App.Api/Models/UserActionRequest.cs new file mode 100644 index 000000000..d94ce7c16 --- /dev/null +++ b/src/Altinn.App.Api/Models/UserActionRequest.cs @@ -0,0 +1,28 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.App.Api.Models; + +/// +/// Request model for user action +/// +public class UserActionRequest +{ + /// + /// Action performed + /// + [JsonPropertyName("action")] + public string? Action { get; set; } + + /// + /// The id of the button that was clicked + /// + [JsonPropertyName("buttonId")] + public string? ButtonId { get; set; } + + /// + /// Additional metadata for the action + /// + [JsonPropertyName("metadata")] + public Dictionary? Metadata { get; set; } +} \ No newline at end of file diff --git a/src/Altinn.App.Api/Models/UserActionResponse.cs b/src/Altinn.App.Api/Models/UserActionResponse.cs new file mode 100644 index 000000000..e05370d38 --- /dev/null +++ b/src/Altinn.App.Api/Models/UserActionResponse.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Text.Json.Serialization; +using Altinn.App.Core.Models.UserAction; + +namespace Altinn.App.Api.Models; + +/// +/// Response object from action endpoint +/// +public class UserActionResponse +{ + /// + /// Data models that have been updated + /// + [JsonPropertyName("updatedDataModels")] + public Dictionary? UpdatedDataModels { get; set; } + + /// + /// Actions frontend should perform after action has been performed backend + /// + [JsonPropertyName("frontendActions")] + public List? FrontendActions { get; set; } + + /// + /// Validation issues that occured when processing action + /// + [JsonPropertyName("error")] + public ActionError? Error { get; set; } +} diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 9285ba537..10c77e740 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -250,8 +250,7 @@ private static void AddProcessServices(IServiceCollection services) private static void AddActionServices(IServiceCollection services) { - services.TryAddTransient(); - services.AddTransient(); + services.TryAddTransient(); services.AddTransient(); services.AddHttpClient(); services.AddTransientUserActionAuthorizerForActionInAllTasks("sign"); diff --git a/src/Altinn.App.Core/Features/Action/NullUserAction.cs b/src/Altinn.App.Core/Features/Action/NullUserAction.cs deleted file mode 100644 index 96f22b589..000000000 --- a/src/Altinn.App.Core/Features/Action/NullUserAction.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Altinn.App.Core.Models.UserAction; - -namespace Altinn.App.Core.Features.Action; - -/// -/// Null action handler for cases where there is no match on the requested -/// -public class NullUserAction: IUserAction -{ - /// - public string Id => "null"; - - /// - public Task HandleAction(UserActionContext context) - { - return Task.FromResult(true); - } -} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs index f450bdf7f..6ee1a37be 100644 --- a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs +++ b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs @@ -42,7 +42,7 @@ public SigningUserAction(IProcessReader processReader, ILogger /// /// - public async Task HandleAction(UserActionContext context) + public async Task HandleAction(UserActionContext context) { if (_processReader.GetFlowElement(context.Instance.Process.CurrentTask.ElementId) is ProcessTask currentTask) { @@ -53,13 +53,17 @@ public async Task HandleAction(UserActionContext context) { SignatureContext signatureContext = new SignatureContext(new InstanceIdentifier(context.Instance), currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType!, await GetSignee(context.UserId), connectedDataElements); await _signClient.SignDataElements(signatureContext); - return true; + return UserActionResult.SuccessResult(); } throw new ApplicationConfigException("Missing configuration for signing. Check that the task has a signature configuration and that the data types to sign are defined."); } - return false; + return UserActionResult.FailureResult(new ActionError() + { + Code = "NoProcessTask", + Message = "Current task is not a process task." + }); } private static List GetDataElementSignatures(List dataElements, List dataTypesToSign) diff --git a/src/Altinn.App.Core/Features/Action/UserActionFactory.cs b/src/Altinn.App.Core/Features/Action/UserActionService.cs similarity index 59% rename from src/Altinn.App.Core/Features/Action/UserActionFactory.cs rename to src/Altinn.App.Core/Features/Action/UserActionService.cs index 636dcc3c0..1e824916a 100644 --- a/src/Altinn.App.Core/Features/Action/UserActionFactory.cs +++ b/src/Altinn.App.Core/Features/Action/UserActionService.cs @@ -1,18 +1,20 @@ +using Altinn.App.Core.Internal; + namespace Altinn.App.Core.Features.Action; /// /// Factory class for resolving implementations /// based on the id of the action. /// -public class UserActionFactory +public class UserActionService { private readonly IEnumerable _actionHandlers; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The list of action handlers to choose from. - public UserActionFactory(IEnumerable actionHandlers) + public UserActionService(IEnumerable actionHandlers) { _actionHandlers = actionHandlers; } @@ -21,14 +23,14 @@ public UserActionFactory(IEnumerable actionHandlers) /// Find the implementation of based on the actionId /// /// The id of the action to handle. - /// The first implementation of that matches the actionId. If no match is returned - public IUserAction GetActionHandler(string? actionId) + /// The first implementation of that matches the actionId. If no match null is returned + public IUserAction? GetActionHandler(string? actionId) { if (actionId != null) { - return _actionHandlers.Where(ah => ah.Id.Equals(actionId, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(new NullUserAction()); + return _actionHandlers.FirstOrDefault(ah => ah.Id.Equals(actionId, StringComparison.OrdinalIgnoreCase)); } - return new NullUserAction(); + return null; } } \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/IUserAction.cs b/src/Altinn.App.Core/Features/IUserAction.cs index 61052b564..9b39a3368 100644 --- a/src/Altinn.App.Core/Features/IUserAction.cs +++ b/src/Altinn.App.Core/Features/IUserAction.cs @@ -17,5 +17,5 @@ public interface IUserAction /// /// The user action context /// If the handling of the action was a success - Task HandleAction(UserActionContext context); + Task HandleAction(UserActionContext context); } \ No newline at end of file diff --git a/src/Altinn.App.Core/Helpers/MultiDecisionHelper.cs b/src/Altinn.App.Core/Helpers/MultiDecisionHelper.cs new file mode 100644 index 000000000..931393556 --- /dev/null +++ b/src/Altinn.App.Core/Helpers/MultiDecisionHelper.cs @@ -0,0 +1,185 @@ +using System.Security.Claims; +using Altinn.App.Core.Models; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Constants; +using Altinn.Common.PEP.Helpers; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Helpers; + +/// +/// Helper class for multi decision requests. +/// +public static class MultiDecisionHelper +{ + private const string XacmlResourceTaskId = "urn:altinn:task"; + private const string XacmlResourceEndId = "urn:altinn:end-event"; + private const string XacmlResourceActionId = "urn:oasis:names:tc:xacml:1.0:action:action-id"; + private const string DefaultIssuer = "Altinn"; + private const string DefaultType = "string"; + private const string SubjectId = "s"; + private const string ActionId = "a"; + private const string ResourceId = "r"; + + /// + /// Creates multi decision request. + /// + public static XacmlJsonRequestRoot CreateMultiDecisionRequest(ClaimsPrincipal user, Instance instance, List actionTypes) + { + ArgumentNullException.ThrowIfNull(user); + + XacmlJsonRequest request = new() + { + AccessSubject = new List() + }; + + request.AccessSubject.Add(CreateMultipleSubjectCategory(user.Claims)); + request.Action = CreateMultipleActionCategory(actionTypes); + request.Resource = CreateMultipleResourceCategory(instance); + request.MultiRequests = CreateMultiRequestsCategory(request.AccessSubject, request.Action, request.Resource); + + XacmlJsonRequestRoot jsonRequest = new() { Request = request }; + + return jsonRequest; + } + + /// + /// Validate a multi decision result and returns a dictionary with the actions and the result. + /// + /// + /// + /// + /// + /// + public static Dictionary ValidatePdpMultiDecision(Dictionary actions, List results, ClaimsPrincipal user) + { + ArgumentNullException.ThrowIfNull(results); + ArgumentNullException.ThrowIfNull(user); + foreach (XacmlJsonResult result in results.Where(r => DecisionHelper.ValidateDecisionResult(r, user))) + { + foreach (var attributes in result.Category.Select(c => c.Attribute)) + { + foreach (var attribute in attributes) + { + if (attribute.AttributeId == XacmlResourceActionId) + { + actions[attribute.Value] = true; + } + } + } + } + + return actions; + } + + private static XacmlJsonCategory CreateMultipleSubjectCategory(IEnumerable claims) + { + XacmlJsonCategory subjectAttributes = DecisionHelper.CreateSubjectCategory(claims); + subjectAttributes.Id = SubjectId + "1"; + + return subjectAttributes; + } + + private static List CreateMultipleActionCategory(List actionTypes) + { + List actionCategories = new(); + int counter = 1; + + foreach (string actionType in actionTypes) + { + XacmlJsonCategory actionCategory; + actionCategory = DecisionHelper.CreateActionCategory(actionType, true); + actionCategory.Id = ActionId + counter.ToString(); + actionCategories.Add(actionCategory); + counter++; + } + + return actionCategories; + } + + private static List CreateMultipleResourceCategory(Instance instance) + { + List resourcesCategories = new(); + int counter = 1; + XacmlJsonCategory resourceCategory = new() { Attribute = new List() }; + + var instanceProps = GetInstanceProperties(instance); + + if (instanceProps.Task != null) + { + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(XacmlResourceTaskId, instanceProps.Task, DefaultType, DefaultIssuer)); + } + else if (instance.Process?.EndEvent != null) + { + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(XacmlResourceEndId, instance.Process.EndEvent, DefaultType, DefaultIssuer)); + } + + if (!string.IsNullOrWhiteSpace(instanceProps.InstanceId)) + { + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(AltinnXacmlUrns.InstanceId, instanceProps.InstanceId, DefaultType, DefaultIssuer, true)); + } + else if (!string.IsNullOrEmpty(instanceProps.InstanceGuid)) + { + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(AltinnXacmlUrns.InstanceId, instanceProps.InstanceOwnerPartyId + "/" + instanceProps.InstanceGuid, DefaultType, DefaultIssuer, true)); + } + + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(AltinnXacmlUrns.PartyId, instanceProps.InstanceOwnerPartyId, DefaultType, DefaultIssuer)); + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(AltinnXacmlUrns.OrgId, instanceProps.appIdentifier.Org, DefaultType, DefaultIssuer)); + resourceCategory.Attribute.Add(DecisionHelper.CreateXacmlJsonAttribute(AltinnXacmlUrns.AppId, instanceProps.appIdentifier.App, DefaultType, DefaultIssuer)); + resourceCategory.Id = ResourceId + counter; + resourcesCategories.Add(resourceCategory); + + return resourcesCategories; + } + + private static (string? InstanceId, string InstanceGuid, string? Task, string InstanceOwnerPartyId, AppIdentifier appIdentifier) GetInstanceProperties(Instance instance) + { + string? instanceId = instance.Id.Contains('/') ? instance.Id : null; + string instanceGuid = instance.Id.Contains('/') ? instance.Id.Split("/")[1] : instance.Id; + string? task = instance.Process?.CurrentTask?.ElementId; + string instanceOwnerPartyId = instance.InstanceOwner.PartyId; + AppIdentifier appIdentifier = new(instance); + return (instanceId, instanceGuid, task, instanceOwnerPartyId, appIdentifier); + } + + private static XacmlJsonMultiRequests CreateMultiRequestsCategory(List subjects, List actions, List resources) + { + List subjectIds = subjects.Select(s => s.Id).ToList(); + List actionIds = actions.Select(a => a.Id).ToList(); + List resourceIds = resources.Select(r => r.Id).ToList(); + + XacmlJsonMultiRequests multiRequests = new() + { + RequestReference = CreateRequestReference(subjectIds, actionIds, resourceIds) + }; + + return multiRequests; + } + + private static List CreateRequestReference(List subjectIds, List actionIds, List resourceIds) + { + List references = new(); + + foreach (string resourceId in resourceIds) + { + foreach (string actionId in actionIds) + { + foreach (string subjectId in subjectIds) + { + XacmlJsonRequestReference reference = new(); + List referenceId = new() + { + subjectId, + actionId, + resourceId + }; + reference.ReferenceId = referenceId; + references.Add(reference); + } + } + } + + return references; + } + +} diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs index 087c6d1b0..dd3906a51 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs @@ -3,19 +3,18 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Models; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Helpers; using Altinn.Common.PEP.Interfaces; using Altinn.Platform.Register.Models; - +using Altinn.Platform.Storage.Interface.Models; using AltinnCore.Authentication.Utils; - using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; - using Newtonsoft.Json; namespace Altinn.App.Core.Infrastructure.Clients.Authorization @@ -119,5 +118,25 @@ public async Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIde bool authorized = DecisionHelper.ValidatePdpDecision(response.Response, user); return authorized; } + + /// + public async Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions) + { + XacmlJsonRequestRoot request = MultiDecisionHelper.CreateMultiDecisionRequest(user, instance, actions); + XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); + if (response?.Response == null) + { + _logger.LogWarning("Failed to get decision from pdp: {SerializeObject}", JsonConvert.SerializeObject(request)); + return new Dictionary(); + } + Dictionary actionsResult = new Dictionary(); + foreach (var action in actions) + { + actionsResult.Add(action, false); + } + return MultiDecisionHelper.ValidatePdpMultiDecision(actionsResult, response.Response, user); + } + + } } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs index a6dd3cde3..97424abe7 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/ProcessClient.cs @@ -46,7 +46,7 @@ public ProcessClient( /// public Stream GetProcessDefinition() { - string bpmnFilePath = _appSettings.AppBasePath + _appSettings.ConfigurationFolder + _appSettings.ProcessFolder + _appSettings.ProcessFileName; + string bpmnFilePath = Path.Join(_appSettings.AppBasePath , _appSettings.ConfigurationFolder , _appSettings.ProcessFolder , _appSettings.ProcessFileName); try { diff --git a/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs b/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs index 50c49cda0..c76e0af62 100644 --- a/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs +++ b/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs @@ -1,8 +1,11 @@ using System.Security.Claims; using Altinn.App.Core.Features.Action; using Altinn.App.Core.Internal.Process.Action; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Auth; @@ -57,6 +60,25 @@ public async Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIde return true; } + /// + public async Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions) + { + var authDecisions = await _authorizationClient.AuthorizeActions(instance, user, actions.Select(a => a.Value).ToList()); + List authorizedActions = new(); + foreach (var action in actions) + { + authorizedActions.Add(new UserAction() + { + Id = action.Value, + Authorized = authDecisions[action.Value], + ActionType = action.ActionType + }); + + } + + return authorizedActions; + } + private static bool IsAuthorizerForTaskAndAction(IUserActionAuthorizerProvider authorizer, string? taskId, string action) { return (authorizer.TaskId == null && authorizer.Action == null) diff --git a/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs b/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs index a4d298836..41fc248bd 100644 --- a/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs +++ b/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs @@ -1,6 +1,9 @@ using System.Security.Claims; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Auth { @@ -34,5 +37,14 @@ public interface IAuthorizationClient /// /// Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null); + + /// + /// Check if the user is authorized to perform the given actions on the given instance. + /// + /// + /// + /// + /// + Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions); } } diff --git a/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs b/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs index 79c89c2be..852467a7c 100644 --- a/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs +++ b/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs @@ -1,6 +1,9 @@ using System.Security.Claims; +using Altinn.App.Core.Internal.Process.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Internal.Auth { @@ -34,5 +37,14 @@ public interface IAuthorizationService /// /// Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null); + + /// + /// Check if the user is authorized to perform the given actions on the given instance. + /// + /// + /// + /// + /// Dictionary with actions and the auth decision + Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions); } } diff --git a/src/Altinn.App.Core/Internal/Exceptions/NotFoundException.cs b/src/Altinn.App.Core/Internal/Exceptions/NotFoundException.cs new file mode 100644 index 000000000..757c3fb7b --- /dev/null +++ b/src/Altinn.App.Core/Internal/Exceptions/NotFoundException.cs @@ -0,0 +1,15 @@ +namespace Altinn.App.Core.Internal.Exceptions; + +/// +/// Exception thrown when a resource is not found +/// +public class NotFoundException: Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// + public NotFoundException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs index 23fa62873..dce18cd68 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs @@ -1,3 +1,5 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; using System.Xml.Serialization; namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties @@ -7,10 +9,62 @@ namespace Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties /// public class AltinnAction { + /// + /// Initializes a new instance of the class + /// + public AltinnAction() + { + Value = string.Empty; + ActionType = ActionType.ProcessAction; + } + /// + /// Initializes a new instance of the class with the given ID + /// + /// + public AltinnAction(string id) + { + Value = id; + ActionType = ActionType.ProcessAction; + } + + /// + /// Initializes a new instance of the class with the given ID and action type + /// + /// + /// + public AltinnAction(string id, ActionType actionType) + { + Value = id; + ActionType = actionType; + } + /// /// Gets or sets the ID of the action /// [XmlText] public string Value { get; set; } + + /// + /// Gets or sets the type of action + /// + [XmlAttribute("type", Namespace = "http://altinn.no/process")] + public ActionType ActionType { get; set; } + } + + /// + /// Defines the different types of actions + /// + public enum ActionType + { + /// + /// The action is a process action + /// + [XmlEnum("processAction")] + ProcessAction, + /// + /// The action is a user action + /// + [XmlEnum("userAction")] + UserAction } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs index 3efb36de2..02e528321 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs @@ -14,6 +14,7 @@ public class AppProcessElementInfo: ProcessElementInfo public AppProcessElementInfo() { Actions = new Dictionary(); + UserActions = new List(); } /// @@ -31,6 +32,7 @@ public AppProcessElementInfo(ProcessElementInfo processElementInfo) Validated = processElementInfo.Validated; FlowType = processElementInfo.FlowType; Actions = new Dictionary(); + UserActions = new List(); } /// /// Actions that can be performed and if the user is allowed to perform them. @@ -38,6 +40,12 @@ public AppProcessElementInfo(ProcessElementInfo processElementInfo) [JsonPropertyName(name:"actions")] public Dictionary? Actions { get; set; } + /// + /// List of available actions for a task, both user and process tasks + /// + [JsonPropertyName(name:"userActions")] + public List UserActions { get; set; } + /// /// Indicates if the user has read access to the task. /// diff --git a/src/Altinn.App.Core/Internal/Process/Elements/UserAction.cs b/src/Altinn.App.Core/Internal/Process/Elements/UserAction.cs new file mode 100644 index 000000000..2556ec2f0 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/UserAction.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; + +namespace Altinn.App.Core.Internal.Process.Elements +{ + /// + /// Defines an altinn action for a task + /// + public class UserAction + { + /// + /// Gets or sets the ID of the action + /// + [JsonPropertyName("id")] + public required string Id { get; set; } + + /// + /// Gets or sets if the user is authorized to perform the action + /// + [JsonPropertyName("authorized")] + public bool Authorized { get; set; } + + /// + /// Gets or sets the type of action + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonPropertyName("type")] + public ActionType ActionType { get; set; } + } +} diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index 08f1ece2f..49e7b8d06 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -22,7 +22,7 @@ public class ProcessEngine : IProcessEngine private readonly IProfileClient _profileClient; private readonly IProcessNavigator _processNavigator; private readonly IProcessEventDispatcher _processEventDispatcher; - private readonly UserActionFactory _userActionFactory; + private readonly UserActionService _userActionService; /// /// Initializes a new instance of the class @@ -31,19 +31,19 @@ public class ProcessEngine : IProcessEngine /// The profile service /// The process navigator /// The process event dispatcher - /// The action handler factory + /// The action handler factory public ProcessEngine( IProcessReader processReader, IProfileClient profileClient, IProcessNavigator processNavigator, IProcessEventDispatcher processEventDispatcher, - UserActionFactory userActionFactory) + UserActionService userActionService) { _processReader = processReader; _profileClient = profileClient; _processNavigator = processNavigator; _processEventDispatcher = processEventDispatcher; - _userActionFactory = userActionFactory; + _userActionService = userActionService; } /// @@ -131,9 +131,11 @@ public async Task Next(ProcessNextRequest request) ErrorType = ProcessErrorType.Conflict }; } - var actionHandler = await _userActionFactory.GetActionHandler(request.Action).HandleAction(new UserActionContext(request.Instance, userId.Value)); - if (!actionHandler) + var actionHandler = _userActionService.GetActionHandler(request.Action); + var actionResult = actionHandler is null ? UserActionResult.SuccessResult() : await actionHandler.HandleAction(new UserActionContext(request.Instance, userId.Value)); + + if (!actionResult.Success) { return new ProcessChangeResult() { diff --git a/src/Altinn.App.Core/Models/UserAction/ActionError.cs b/src/Altinn.App.Core/Models/UserAction/ActionError.cs new file mode 100644 index 000000000..1b249832e --- /dev/null +++ b/src/Altinn.App.Core/Models/UserAction/ActionError.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace Altinn.App.Core.Models.UserAction; + +/// +/// Defines an error object that should be returned if the action fails +/// +public class ActionError +{ + /// + /// Machine readable error code + /// + [JsonPropertyName("code")] + public string? Code { get; set; } + + /// + /// Human readable error message or text key + /// + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + /// + /// Error metadata + /// + [JsonPropertyName("metadata")] + public Dictionary? Metadata { get; set; } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Models/UserAction/FrontendAction.cs b/src/Altinn.App.Core/Models/UserAction/FrontendAction.cs new file mode 100644 index 000000000..d1743122b --- /dev/null +++ b/src/Altinn.App.Core/Models/UserAction/FrontendAction.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace Altinn.App.Core.Models.UserAction; + +/// +/// Defines an action that should be performed by frontend +/// +public class FrontendAction +{ + /// + /// Name of the action + /// + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// + /// Metadata for the action + /// + [JsonPropertyName("metadata")] + public Dictionary? Metadata { get; set; } + + /// + /// Creates a nextPage frontend action + /// + /// + public static FrontendAction NextPage() + { + var frontendAction = new FrontendAction() + { + Name = "nextPage" + }; + return frontendAction; + } +} diff --git a/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs b/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs index 1076c49e2..a76293229 100644 --- a/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs +++ b/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs @@ -12,10 +12,14 @@ public class UserActionContext /// /// The instance the action is performed on /// The user performing the action - public UserActionContext(Instance instance, int userId) + /// The id of the button that triggered the action (optional) + /// + public UserActionContext(Instance instance, int userId, string? buttonId = null, Dictionary? actionMetadata = null) { Instance = instance; UserId = userId; + ButtonId = buttonId; + ActionMetadata = actionMetadata ?? new Dictionary(); } /// @@ -27,4 +31,14 @@ public UserActionContext(Instance instance, int userId) /// The user performing the action /// public int UserId { get; } + + /// + /// The id of the button that triggered the action (optional) + /// + public string? ButtonId { get; } + + /// + /// Additional metadata for the action + /// + public Dictionary ActionMetadata { get; } } \ 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 new file mode 100644 index 000000000..02d9605aa --- /dev/null +++ b/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs @@ -0,0 +1,76 @@ +using System.Net; +using System.Runtime.Serialization; +using Altinn.App.Core.Models.Validation; + +namespace Altinn.App.Core.Models.UserAction; + +/// +/// Represents the result of a user action +/// +public class UserActionResult +{ + /// + /// Gets or sets a value indicating whether the user action was a success + /// + public bool Success { get; set; } + + /// + /// Gets or sets a dictionary of updated data models. Key should be dataTypeId + /// + public Dictionary? UpdatedDataModels { get; set; } + + /// + /// Actions for the frontend to perform after the user action has been handled + /// + public List? FrontendActions { get; set; } + + /// + /// Validation issues that should be displayed to the user + /// + public ActionError? Error { get; set; } + + /// + /// Creates a success result + /// + /// + /// + public static UserActionResult SuccessResult(List? frontendActions = null) + { + var userActionResult = new UserActionResult + { + Success = true, + FrontendActions = frontendActions + }; + return userActionResult; + } + + /// + /// Creates a failure result + /// + /// + /// + /// + public static UserActionResult FailureResult(ActionError error, List? frontendActions = null) + { + return new UserActionResult + { + Success = false, + FrontendActions = frontendActions, + Error = error + }; + } + + /// + /// Adds an updated data model to the result + /// + /// + /// + public void AddUpdatedDataModel(string dataModelId, object? dataModel) + { + if (UpdatedDataModels == null) + { + UpdatedDataModels = new Dictionary(); + } + UpdatedDataModels.Add(dataModelId, dataModel); + } +} diff --git a/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs new file mode 100644 index 000000000..1321186c6 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs @@ -0,0 +1,207 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Api.Tests.Utils; +using Altinn.App.Core.Features; +using Altinn.App.Core.Models.UserAction; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Altinn.App.Api.Tests.Controllers; + +public class ActionsControllerTests: ApiTestBase, IClassFixture> +{ + public ActionsControllerTests(WebApplicationFactory factory) : base(factory) + { + } + + [Fact] + public async Task Perform_returns_403_if_user_not_authorized() + { + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1000, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Perform_returns_401_if_user_not_authenticated() + { + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Perform_returns_401_if_userId_is_null() + { + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(null, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Perform_returns_400_if_action_is_null() + { + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1000, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":null}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task Perform_returns_409_if_process_not_started() + { + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef43"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1000, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.Conflict); + } + + [Fact] + public async Task Perform_returns_409_if_process_ended() + { + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef42"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1000, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.Conflict); + } + + [Fact] + public async Task Perform_returns_200_if_action_succeeded() + { + OverrideServicesForThisTest = (services) => + { + services.AddTransient(); + }; + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1000, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [Fact] + public async Task Perform_returns_400_if_action_failed() + { + OverrideServicesForThisTest = (services) => + { + services.AddTransient(); + }; + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1001, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task Perform_returns_404_if_action_implementation_not_found() + { + OverrideServicesForThisTest = (services) => + { + services.AddTransient(); + }; + var org = "tdd"; + var app = "task-action"; + HttpClient client = GetRootedClient(org, app); + Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + string token = PrincipalUtil.GetToken(1001, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"notfound\"}", Encoding.UTF8, "application/json")); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } +} + +public class LookupAction : IUserAction +{ + public string Id => "lookup"; + public async Task HandleAction(UserActionContext context) + { + await Task.CompletedTask; + if (context.UserId == 1000) + { + return UserActionResult.SuccessResult(); + } + + return UserActionResult.FailureResult(new ActionError()); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef41.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef41.pretest.json new file mode 100644 index 000000000..69ac2d877 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef41.pretest.json @@ -0,0 +1,39 @@ +{ + "id": "1337/b1135209-628e-4a6e-9efd-e4282068ef41", + "instanceOwner": { + "partyId": "1337", + "personNumber": "01039012345" + }, + "appId": "tdd/task-action", + "org": "tdd", + "process": { + "started": "2019-12-05T13:24:34.8412179Z", + "startEvent": "StartEvent_1", + "currentTask": { + "flow": 2, + "started": "2019-12-05T13:24:34.9196661Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "validated": { + "timestamp": "2020-02-07T10:46:36.985894+01:00", + "canCompleteTask": false + } + } + }, + "status": { + "isArchived": false, + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + }, + "data": [ + { + "id": "de288942-a8af-4f77-a1f1-6e1ede1cd502", + "dataType": "default", + "contentType": "application/xml", + "size": 0, + "locked": false + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef42.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef42.pretest.json new file mode 100644 index 000000000..e0c090e9c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef42.pretest.json @@ -0,0 +1,31 @@ +{ + "id": "1337/b1135209-628e-4a6e-9efd-e4282068ef41", + "instanceOwner": { + "partyId": "1337", + "personNumber": "01039012345" + }, + "appId": "tdd/task-action", + "org": "tdd", + "process": { + "started": "2023-11-15T09:47:36.21031Z", + "startEvent": "StartEvent_1", + "ended": "2023-11-15T09:47:39.979157Z", + "endEvent": "EndEvent_1" + }, + "status": { + "isArchived": true, + "archived": "2023-11-15T09:47:39.979157Z", + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + }, + "data": [ + { + "id": "de288942-a8af-4f77-a1f1-6e1ede1cd502", + "dataType": "default", + "contentType": "application/xml", + "size": 0, + "locked": false + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef43.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef43.pretest.json new file mode 100644 index 000000000..6b17a70bf --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/task-action/1337/b1135209-628e-4a6e-9efd-e4282068ef43.pretest.json @@ -0,0 +1,26 @@ +{ + "id": "1337/b1135209-628e-4a6e-9efd-e4282068ef41", + "instanceOwner": { + "partyId": "1337", + "personNumber": "01039012345" + }, + "appId": "tdd/task-action", + "org": "tdd", + "process": null, + "status": { + "isArchived": true, + "archived": "2023-11-15T09:47:39.979157Z", + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + }, + "data": [ + { + "id": "de288942-a8af-4f77-a1f1-6e1ede1cd502", + "dataType": "default", + "contentType": "application/xml", + "size": 0, + "locked": false + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/appsettings.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/appsettings.json new file mode 100644 index 000000000..b1440bc4f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/appsettings.json @@ -0,0 +1,37 @@ +{ + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://*:5005" + } + } + }, + "AppSettings": { + "OpenIdWellKnownEndpoint": "http://localhost:5101/authentication/api/v1/openid/", + "Hostname": "altinn3local.no", + "RuntimeCookieName": "AltinnStudioRuntime", + "RegisterEventsWithEventsComponent": false, + "EnableEFormidling": true + }, + "GeneralSettings": { + "HostName": "altinn3local.no", + "SoftValidationPrefix": "*WARNING*", + "AltinnPartyCookieName": "AltinnPartyId" + }, + "EFormidlingClientSettings": { + "BaseUrl": "http://localhost:9093/api/" + }, + "PlatformSettings": { + "ApiStorageEndpoint": "http://localhost:5101/storage/api/v1/", + "ApiRegisterEndpoint": "http://localhost:5101/register/api/v1/", + "ApiProfileEndpoint": "http://localhost:5101/profile/api/v1/", + "ApiAuthenticationEndpoint": "http://localhost:5101/authentication/api/v1/", + "ApiAuthorizationEndpoint": "http://localhost:5101/authorization/api/v1/", + "ApiEventsEndpoint": "http://localhost:5101/events/api/v1/", + "ApiPdfEndpoint": "http://localhost:5070/api/v1/", + "SubscriptionKey": "retrieved from environment at runtime" + }, + "ApplicationInsights": { + "InstrumentationKey": "retrieved from environment at runtime" + } +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/applicationmetadata.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/applicationmetadata.json new file mode 100644 index 000000000..04fdae29e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/applicationmetadata.json @@ -0,0 +1,53 @@ +{ + "id": "ttd/task-action", + "org": "ttd", + "title": { + "nb": "task-action" + }, + "dataTypes": [ + { + "id": "ref-data-as-pdf", + "allowedContentTypes": [ + "application/pdf" + ], + "maxCount": 0, + "minCount": 0, + "enablePdfCreation": true, + "enableFileScan": false, + "validationErrorOnPendingFileScan": false, + "enabledFileAnalysers": [], + "enabledFileValidators": [] + }, + { + "id": "Scheme", + "allowedContentTypes": [ + "application/xml" + ], + "appLogic": { + "autoCreate": true, + "classRef": "Altinn.App.Models.Scheme", + "allowAnonymousOnStateless": false, + "autoDeleteOnProcessEnd": false + }, + "taskId": "Task_1", + "maxCount": 1, + "minCount": 1, + "enablePdfCreation": true, + "enableFileScan": false, + "validationErrorOnPendingFileScan": false, + "enabledFileAnalysers": [], + "enabledFileValidators": [] + } + ], + "partyTypesAllowed": { + "bankruptcyEstate": false, + "organisation": false, + "person": false, + "subUnit": false + }, + "autoDeleteOnProcessEnd": false, + "created": "2023-05-31T08:03:25.9385888Z", + "createdBy": "tjololo", + "lastChanged": "2023-05-31T08:03:25.9385925Z", + "lastChangedBy": "tjololo" +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/authorization/policy.xml b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/authorization/policy.xml new file mode 100644 index 000000000..3e3697176 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/authorization/policy.xml @@ -0,0 +1,401 @@ + + + + + A rule giving user with role REGNA or DAGL and the app owner tdd the right to instantiate a + instance of a given app of tdd/task-actions + + + + + + REGNA + + + + + + DAGL + + + + + + tdd + + + + + + + + tdd + + + + task-actions + + + + + + + + instantiate + + + + + + read + + + + + + + + Rule that defines that user with role REGNA or DAGL can read, write, lookup, toconfirm and complete for + tdd/task-actions when it is in Task_1 + + + + + + REGNA + + + + + + DAGL + + + + + + + + tdd + + + + task-actions + + + + Task_1 + + + + + + + + read + + + + + + write + + + + + + complete + + + + + + lookup + + + + + + + + Rule that defines that user with role REGNA or DAGL can delete instances of tdd/task-actions + + + + + + REGNA + + + + + + DAGL + + + + + + + + tdd + + + + task-actions + + + + delete + + + + + + + + Rule that defines that org can write to instances of tdd/task-actions for any states + + + + + + tdd + + + + + + + + tdd + + + + task-actions + + + + + + + + write + + + + + + + + Rule that defines that org can complete an instance of tdd/task-actions which state is at the end + event. + + + + + + tdd + + + + + + + + tdd + + + + task-actions + + + + EndEvent_1 + + + + + + + + complete + + + + + + + + A rule giving user with role REGNA or DAGL and the app owner tdd the right to read the + appresource events of a given app of tdd/task-actions + + + + + + REGNA + + + + + + DAGL + + + + + + tdd + + + + + + + + tdd + + + + task-actions + + + + events + + + + + + + + read + + + + + + + + + + 2 + + + + diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/process/process.bpmn b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/process/process.bpmn new file mode 100644 index 000000000..4a02f91aa --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/process/process.bpmn @@ -0,0 +1,26 @@ + + + + + SequenceFlow_1 + + + SequenceFlow_1 + SequenceFlow_2 + + + data + + complete + lookup + + + + + + SequenceFlow_2 + + + + + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/texts/resource.nb.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/texts/resource.nb.json new file mode 100644 index 000000000..0a628564e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/texts/resource.nb.json @@ -0,0 +1,33 @@ +{ + "language": "nb", + "resources": [ + { + "id": "appName", + "value": "vga-simple-app" + }, + { + "id": "Side1.Input-P4VNaW.title", + "value": "Navn" + }, + { + "id": "Side1.Input-P4VNaX.title", + "value": "Regex validation" + }, + { + "id": "Side1.Button-Lj0cYD.title", + "value": "Til signering" + }, + { + "id": "Side1.Button-Lj0cYE.title", + "value": "Fullfør" + }, + { + "id": "Side1.Button-Lj0cYF.title", + "value": "Bekreft" + }, + { + "id": "Side1.Button-Lj0cYG.title", + "value": "Avvis" + } + ] +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs index 39ed807f8..43ce27de8 100644 --- a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Models; +using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Api.Tests.Mocks { @@ -19,7 +20,28 @@ public class AuthorizationMock : IAuthorizationClient return Task.FromResult(isvalid); } + /// + /// Mock method that returns false for actions ending with _unauthorized, and true for all other actions. + /// + /// + /// + /// + /// + /// + /// + /// public async Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, ClaimsPrincipal user, string action, string? taskId = null) + { + await Task.CompletedTask; + if(action.EndsWith("_unauthorized")) + { + return false; + } + + return true; + } + + public Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions) { throw new NotImplementedException(); } diff --git a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs index 2a86d6c77..a0dde5e9a 100644 --- a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs +++ b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs @@ -7,21 +7,25 @@ namespace Altinn.App.Api.Tests.Utils { public static class PrincipalUtil { - public static string GetToken(int userId, int authenticationLevel = 2) + public static string GetToken(int? userId, int authenticationLevel = 2) { ClaimsPrincipal principal = GetUserPrincipal(userId, authenticationLevel); string token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); return token; } - public static ClaimsPrincipal GetUserPrincipal(int userId, int authenticationLevel = 2) + public static ClaimsPrincipal GetUserPrincipal(int? userId, int authenticationLevel = 2) { List claims = new List(); string issuer = "www.altinn.no"; - claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString(), ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.UserId, userId.ToString(), ClaimValueTypes.String, issuer)); + if (userId != null) + { + claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString()!, ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.UserId, userId.ToString()!, ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.PartyID, userId.ToString()!, ClaimValueTypes.Integer32, issuer)); + } + claims.Add(new Claim(AltinnCoreClaimTypes.UserName, "UserOne", ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.PartyID, userId.ToString(), ClaimValueTypes.Integer32, issuer)); claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticationLevel, authenticationLevel.ToString(), ClaimValueTypes.Integer32, issuer)); diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index a5a21bde6..c6a1d9fc2 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -71,7 +71,10 @@ - + + Always + + Always diff --git a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs index 7b77a6c26..197b00cc0 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs @@ -64,7 +64,7 @@ public async void HandleAction_returns_ok_if_user_is_valid() // Assert SignatureContext expected = new SignatureContext(new InstanceIdentifier(instance), "signature", new Signee() { UserId = "1337", PersonNumber = "12345678901" }, new DataElementSignature("a499c3ef-e88a-436b-8650-1c43e5037ada")); signClientMock.Verify(s => s.SignDataElements(It.Is(sc => AssertSigningContextAsExpected(sc, expected))), Times.Once); - result.Should().BeTrue(); + result.Should().BeEquivalentTo(UserActionResult.SuccessResult()); signClientMock.VerifyNoOtherCalls(); } diff --git a/test/Altinn.App.Core.Tests/Features/Action/UserActionFactoryTests.cs b/test/Altinn.App.Core.Tests/Features/Action/UserActionFactoryTests.cs deleted file mode 100644 index af038b845..000000000 --- a/test/Altinn.App.Core.Tests/Features/Action/UserActionFactoryTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Altinn.App.Core.Features; -using Altinn.App.Core.Features.Action; -using Altinn.App.Core.Models.UserAction; -using FluentAssertions; -using Xunit; - -namespace Altinn.App.Core.Tests.Features.Action; - -public class UserActionFactoryTests -{ - [Fact] - public void GetActionHandler_should_return_DummyActionHandler_for_id_dummy() - { - var factory = new UserActionFactory(new List() { new DummyUserAction() }); - - IUserAction userAction = factory.GetActionHandler("dummy"); - - userAction.Should().BeOfType(); - userAction.Id.Should().Be("dummy"); - } - - [Fact] - public void GetActionHandler_should_return_first_DummyActionHandler_for_id_dummy_if_multiple() - { - var factory = new UserActionFactory(new List() { new DummyUserAction(), new DummyUserAction2() }); - - IUserAction userAction = factory.GetActionHandler("dummy"); - - userAction.Should().BeOfType(); - userAction.Id.Should().Be("dummy"); - } - - [Fact] - public void GetActionHandler_should_return_NullActionHandler_if_id_not_found() - { - var factory = new UserActionFactory(new List() { new DummyUserAction() }); - - IUserAction userAction = factory.GetActionHandler("nonexisting"); - - userAction.Should().BeOfType(); - userAction.Id.Should().Be("null"); - } - - [Fact] - public void GetActionHandler_should_return_NullActionHandler_if_id_is_null() - { - var factory = new UserActionFactory(new List() { new DummyUserAction() }); - - IUserAction userAction = factory.GetActionHandler(null); - - userAction.Should().BeOfType(); - userAction.Id.Should().Be("null"); - } - - internal class DummyUserAction : IUserAction - { - public string Id { get; set; } = "dummy"; - - public Task HandleAction(UserActionContext context) - { - return Task.FromResult(true); - } - } - - internal class DummyUserAction2 : IUserAction - { - public string Id { get; set; } = "dummy"; - - public Task HandleAction(UserActionContext context) - { - return Task.FromResult(true); - } - } -} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs new file mode 100644 index 000000000..206a66835 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs @@ -0,0 +1,75 @@ +#nullable enable +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Models.UserAction; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Action; + +public class UserActionServiceTests +{ + [Fact] + public void GetActionHandlerOrDefault_should_return_DummyActionHandler_for_id_dummy() + { + var factory = new UserActionService(new List() { new DummyUserAction() }); + + IUserAction? userAction = factory.GetActionHandler("dummy"); + + userAction.Should().NotBeNull(); + userAction.Should().BeOfType(); + userAction!.Id.Should().Be("dummy"); + } + + [Fact] + public void GetActionHandlerOrDefault_should_return_first_DummyActionHandler_for_id_dummy_if_multiple() + { + var factory = new UserActionService(new List() { new DummyUserAction(), new DummyUserAction2() }); + + IUserAction? userAction = factory.GetActionHandler("dummy"); + + userAction.Should().NotBeNull(); + userAction.Should().BeOfType(); + userAction!.Id.Should().Be("dummy"); + } + + [Fact] + public void GetActionHandlerOrDefault_should_return_null_if_id_not_found_and_default_not_set() + { + var factory = new UserActionService(new List() { new DummyUserAction() }); + + IUserAction? userAction = factory.GetActionHandler("nonexisting"); + + userAction.Should().BeNull(); + } + + [Fact] + public void GetActionHandlerOrDefault_should_return_null_if_id_is_null_and_default_not_set() + { + var factory = new UserActionService(new List() { new DummyUserAction() }); + + IUserAction? userAction = factory.GetActionHandler(null); + + userAction.Should().BeNull(); + } + + internal class DummyUserAction : IUserAction + { + public string Id => "dummy"; + + public Task HandleAction(UserActionContext context) + { + return Task.FromResult(UserActionResult.SuccessResult()); + } + } + + internal class DummyUserAction2 : IUserAction + { + public string Id => "dummy"; + + public Task HandleAction(UserActionContext context) + { + return Task.FromResult(UserActionResult.SuccessResult()); + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs b/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs new file mode 100644 index 000000000..20d60e5e3 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/MultiDecisionHelperTests.cs @@ -0,0 +1,265 @@ +using System.Security.Claims; +using System.Text.Json; +using Altinn.App.Core.Helpers; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Helpers; + +public class MultiDecisionHelperTests +{ + private static readonly JsonSerializerOptions SerializerOptions = new() + { + WriteIndented = true + }; + + [Fact] + public void CreateMultiDecisionRequest_generates_multidecisionrequest_with_all_actions_current_task_elemtnId() + { + var claimsPrincipal = GetClaims("1337"); + + var instance = new Instance() + { + Id = "1337/1dd16477-187b-463c-8adf-592c7fa78459", + Org = "tdd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + AppId = "tdd/test-app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + AltinnTaskType = "Data", + ElementId = "Task_1" + }, + EndEvent = "EndEvent_1" + } + }; + + var actions = new List() + { + "sign", + "reject" + }; + + var result = MultiDecisionHelper.CreateMultiDecisionRequest(claimsPrincipal, instance, actions); + + CompareWithOrUpdateGoldenFile("multidecision-all-actions-task", result); + } + + [Fact] + public void CreateMultiDecisionRequest_generates_multidecisionrequest_with_all_actions_instanceId_is_GUID_only() + { + var claimsPrincipal = GetClaims("1337"); + + var instance = new Instance() + { + Id = "1dd16477-187b-463c-8adf-592c7fa78459", + Org = "tdd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + AppId = "tdd/test-app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + AltinnTaskType = "Data", + ElementId = "Task_1" + }, + EndEvent = "EndEvent_1" + } + }; + + var actions = new List() + { + "sign", + "reject" + }; + + var result = MultiDecisionHelper.CreateMultiDecisionRequest(claimsPrincipal, instance, actions); + + CompareWithOrUpdateGoldenFile("multidecision-all-actions-guid", result); + } + + [Fact] + public void CreateMultiDecisionRequest_generates_multidecisionrequest_with_all_actions_endevent() + { + var claimsPrincipal = GetClaims("1337"); + + var instance = new Instance() + { + Id = "1337/1dd16477-187b-463c-8adf-592c7fa78459", + Org = "tdd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + AppId = "tdd/test-app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + AltinnTaskType = "Task_1" + }, + EndEvent = "EndEvent_1" + } + }; + + var actions = new List() + { + "sign", + "reject" + }; + + var result = MultiDecisionHelper.CreateMultiDecisionRequest(claimsPrincipal, instance, actions); + + CompareWithOrUpdateGoldenFile("multidecision-all-actions-endevent", result); + } + + [Fact] + public void CreateMultiDecisionRequest_throws_ArgumentNullException_if_user_is_null() + { + var instance = new Instance() + { + Id = "1337/1dd16477-187b-463c-8adf-592c7fa78459", + Org = "tdd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + AppId = "tdd/test-app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + AltinnTaskType = "Task_1" + }, + EndEvent = "EndEvent_1" + } + }; + + var actions = new List() + { + "sign", + "reject" + }; + Action act = () => MultiDecisionHelper.CreateMultiDecisionRequest(null, instance, actions); + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'user')"); + } + + [Fact] + public void ValidateDecisionResult_all_actions_allowed() + { + var response = GetXacmlJsonRespons("all-actions-allowed"); + var expected = new Dictionary() + { + { "read", true }, + { "write", true }, + { "complete", true }, + { "lookup", true } + }; + var actions = new Dictionary() + { + { "read", false }, + { "write", false }, + { "complete", false }, + { "lookup", false } + }; + var result = MultiDecisionHelper.ValidatePdpMultiDecision(actions, response, GetClaims("501337")); + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void ValidateDecisionResult_one_action_denied() + { + var response = GetXacmlJsonRespons("one-action-denied"); + var expected = new Dictionary() + { + { "read", true }, + { "write", true }, + { "complete", true }, + { "lookup", false } + }; + var actions = new Dictionary() + { + { "read", false }, + { "write", false }, + { "complete", false }, + { "lookup", false } + }; + var result = MultiDecisionHelper.ValidatePdpMultiDecision(actions, response, GetClaims("501337")); + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void ValidateDecisionResult_throws_ArgumentNullException_if_response_is_null() + { + var actions = new Dictionary() + { + { "read", false }, + { "write", false }, + { "complete", false }, + { "lookup", false } + }; + Action act = () => MultiDecisionHelper.ValidatePdpMultiDecision(actions, null, GetClaims("501337")); + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'results')"); + } + + [Fact] + public void ValidateDecisionResult_throws_ArgumentNullException_if_user_is_null() + { + var response = GetXacmlJsonRespons("one-action-denied"); + var actions = new Dictionary() + { + { "read", false }, + { "write", false }, + { "complete", false }, + { "lookup", false } + }; + Action act = () => MultiDecisionHelper.ValidatePdpMultiDecision(actions, response, null); + act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'user')"); + } + + private static ClaimsPrincipal GetClaims(string partyId) + { + return new ClaimsPrincipal(new List() + { + new(new List + { + new("urn:altinn:partyid", partyId, "#integer"), + new("urn:altinn:authlevel", "3", "#integer"), + }) + }); + } + + private static string XacmlJsonRequestRootToString(XacmlJsonRequestRoot request) + { + return JsonSerializer.Serialize(request, SerializerOptions); + } + + private static void CompareWithOrUpdateGoldenFile(string testId, XacmlJsonRequestRoot xacmlJsonRequestRoot) + { + bool updateGoldeFiles = Environment.GetEnvironmentVariable("UpdateGoldenFiles") == "true"; + string goldenFilePath = Path.Join("Helpers", "TestData", "MultiDecisionHelper", testId + ".golden.json"); + string xacmlJsonRequestRootAsString = XacmlJsonRequestRootToString(xacmlJsonRequestRoot); + if (updateGoldeFiles) + { + File.WriteAllText(goldenFilePath, xacmlJsonRequestRootAsString); + } + + string goldenFileContent = File.ReadAllText(goldenFilePath); + Assert.Equal(goldenFileContent, xacmlJsonRequestRootAsString); + } + + private static List GetXacmlJsonRespons(string filename) + { + var xacmlJesonRespons = File.ReadAllText(Path.Join("Helpers", "TestData", "MultiDecisionHelper", filename + ".json")); + return JsonSerializer.Deserialize>(xacmlJesonRespons); + } +} diff --git a/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/all-actions-allowed.json b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/all-actions-allowed.json new file mode 100644 index 000000000..f4435cf59 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/all-actions-allowed.json @@ -0,0 +1,230 @@ +[ + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "write", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "complete", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "lookup", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + } +] \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-endevent.golden.json b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-endevent.golden.json new file mode 100644 index 000000000..ac3952e0c --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-endevent.golden.json @@ -0,0 +1,126 @@ +{ + "Request": { + "ReturnPolicyIdList": false, + "CombinedDecision": false, + "XPathVersion": null, + "Category": null, + "Resource": [ + { + "CategoryId": null, + "Id": "r1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:end-event", + "Value": "EndEvent_1", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:instance-id", + "Value": "1337/1dd16477-187b-463c-8adf-592c7fa78459", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + }, + { + "AttributeId": "urn:altinn:partyid", + "Value": "1337", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:org", + "Value": "tdd", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:app", + "Value": "test-app", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + } + ] + } + ], + "Action": [ + { + "CategoryId": null, + "Id": "a1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "sign", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + } + ] + }, + { + "CategoryId": null, + "Id": "a2", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "reject", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + } + ] + } + ], + "AccessSubject": [ + { + "CategoryId": null, + "Id": "s1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:partyid", + "Value": "1337", + "Issuer": "LOCAL AUTHORITY", + "DataType": "#integer", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:authlevel", + "Value": "3", + "Issuer": "LOCAL AUTHORITY", + "DataType": "#integer", + "IncludeInResult": false + } + ] + } + ], + "RecipientSubject": null, + "IntermediarySubject": null, + "RequestingMachine": null, + "MultiRequests": { + "RequestReference": [ + { + "ReferenceId": [ + "s1", + "a1", + "r1" + ] + }, + { + "ReferenceId": [ + "s1", + "a2", + "r1" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-guid.golden.json b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-guid.golden.json new file mode 100644 index 000000000..0acdb5dc3 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-guid.golden.json @@ -0,0 +1,126 @@ +{ + "Request": { + "ReturnPolicyIdList": false, + "CombinedDecision": false, + "XPathVersion": null, + "Category": null, + "Resource": [ + { + "CategoryId": null, + "Id": "r1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:task", + "Value": "Task_1", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:instance-id", + "Value": "1337/1dd16477-187b-463c-8adf-592c7fa78459", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + }, + { + "AttributeId": "urn:altinn:partyid", + "Value": "1337", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:org", + "Value": "tdd", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:app", + "Value": "test-app", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + } + ] + } + ], + "Action": [ + { + "CategoryId": null, + "Id": "a1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "sign", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + } + ] + }, + { + "CategoryId": null, + "Id": "a2", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "reject", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + } + ] + } + ], + "AccessSubject": [ + { + "CategoryId": null, + "Id": "s1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:partyid", + "Value": "1337", + "Issuer": "LOCAL AUTHORITY", + "DataType": "#integer", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:authlevel", + "Value": "3", + "Issuer": "LOCAL AUTHORITY", + "DataType": "#integer", + "IncludeInResult": false + } + ] + } + ], + "RecipientSubject": null, + "IntermediarySubject": null, + "RequestingMachine": null, + "MultiRequests": { + "RequestReference": [ + { + "ReferenceId": [ + "s1", + "a1", + "r1" + ] + }, + { + "ReferenceId": [ + "s1", + "a2", + "r1" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-task.golden.json b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-task.golden.json new file mode 100644 index 000000000..0acdb5dc3 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/multidecision-all-actions-task.golden.json @@ -0,0 +1,126 @@ +{ + "Request": { + "ReturnPolicyIdList": false, + "CombinedDecision": false, + "XPathVersion": null, + "Category": null, + "Resource": [ + { + "CategoryId": null, + "Id": "r1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:task", + "Value": "Task_1", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:instance-id", + "Value": "1337/1dd16477-187b-463c-8adf-592c7fa78459", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + }, + { + "AttributeId": "urn:altinn:partyid", + "Value": "1337", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:org", + "Value": "tdd", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:app", + "Value": "test-app", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": false + } + ] + } + ], + "Action": [ + { + "CategoryId": null, + "Id": "a1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "sign", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + } + ] + }, + { + "CategoryId": null, + "Id": "a2", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "reject", + "Issuer": "Altinn", + "DataType": "string", + "IncludeInResult": true + } + ] + } + ], + "AccessSubject": [ + { + "CategoryId": null, + "Id": "s1", + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:partyid", + "Value": "1337", + "Issuer": "LOCAL AUTHORITY", + "DataType": "#integer", + "IncludeInResult": false + }, + { + "AttributeId": "urn:altinn:authlevel", + "Value": "3", + "Issuer": "LOCAL AUTHORITY", + "DataType": "#integer", + "IncludeInResult": false + } + ] + } + ], + "RecipientSubject": null, + "IntermediarySubject": null, + "RequestingMachine": null, + "MultiRequests": { + "RequestReference": [ + { + "ReferenceId": [ + "s1", + "a1", + "r1" + ] + }, + { + "ReferenceId": [ + "s1", + "a2", + "r1" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/one-action-denied.json b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/one-action-denied.json new file mode 100644 index 000000000..d8eff9c09 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/TestData/MultiDecisionHelper/one-action-denied.json @@ -0,0 +1,188 @@ +[ + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "write", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "complete", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "NotApplicable", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": null, + "AssociateAdvice": null, + "Category": null, + "PolicyIdentifierList": null + } +] diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs new file mode 100644 index 000000000..55ce279e2 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs @@ -0,0 +1,132 @@ +using System.Runtime.InteropServices.JavaScript; +using System.Security.Claims; +using System.Text.Json; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Infrastructure.Clients.Authorization; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Interfaces; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Infrastructure.Clients.Authorization; + +public class AuthorizationClientTests +{ + [Fact] + public async Task AuthorizeActions_returns_dictionary_with_one_action_denied() + { + Mock pdpMock = new(); + Mock httpContextAccessorMock = new(); + Mock httpClientMock = new(); + Mock> appSettingsMock = new(); + var pdpResponse = GetXacmlJsonRespons("one-action-denied"); + pdpMock.Setup(s => s.GetDecisionForRequest(It.IsAny())).ReturnsAsync(pdpResponse); + AuthorizationClient client = new AuthorizationClient(Options.Create(new PlatformSettings()), httpContextAccessorMock.Object, httpClientMock.Object, appSettingsMock.Object, pdpMock.Object, NullLogger.Instance); + + var claimsPrincipal = GetClaims("1337"); + + var instance = new Instance() + { + Id = "1337/1dd16477-187b-463c-8adf-592c7fa78459", + Org = "tdd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + AppId = "tdd/test-app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + AltinnTaskType = "Data", + ElementId = "Task_1" + }, + EndEvent = "EndEvent_1" + } + }; + + var expected = new Dictionary() + { + { "read", true }, + { "write", true }, + { "complete", true }, + { "lookup", false } + }; + var actions = new List() + { + "read", + "write", + "complete", + "lookup" + }; + var actual = await client.AuthorizeActions(instance, claimsPrincipal, actions); + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public async Task AuthorizeActions_returns_empty_dictionary_if_no_response_from_pdp() + { + Mock pdpMock = new(); + Mock httpContextAccessorMock = new(); + Mock httpClientMock = new(); + Mock> appSettingsMock = new(); + pdpMock.Setup(s => s.GetDecisionForRequest(It.IsAny())).ReturnsAsync(new XacmlJsonResponse()); + AuthorizationClient client = new AuthorizationClient(Options.Create(new PlatformSettings()), httpContextAccessorMock.Object, httpClientMock.Object, appSettingsMock.Object, pdpMock.Object, NullLogger.Instance); + + var claimsPrincipal = GetClaims("1337"); + + var instance = new Instance() + { + Id = "1337/1dd16477-187b-463c-8adf-592c7fa78459", + Org = "tdd", + InstanceOwner = new InstanceOwner() + { + PartyId = "1337" + }, + AppId = "tdd/test-app", + Process = new ProcessState() + { + CurrentTask = new ProcessElementInfo() + { + AltinnTaskType = "Data", + ElementId = "Task_1" + }, + EndEvent = "EndEvent_1" + } + }; + + var expected = new Dictionary(); + var actions = new List() + { + "read", + "write", + "complete", + "lookup" + }; + var actual = await client.AuthorizeActions(instance, claimsPrincipal, actions); + actual.Should().BeEquivalentTo(expected); + } + + private static ClaimsPrincipal GetClaims(string partyId) + { + return new ClaimsPrincipal(new List() + { + new(new List + { + new("urn:altinn:partyid", partyId, "#integer"), + new("urn:altinn:authlevel", "3", "#integer"), + }) + }); + } + + private static XacmlJsonResponse GetXacmlJsonRespons(string filename) + { + var xacmlJesonRespons = File.ReadAllText(Path.Join("Infrastructure", "Clients", "Authorization", "TestData", $"{filename}.json")); + return JsonSerializer.Deserialize(xacmlJesonRespons); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/TestData/one-action-denied.json b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/TestData/one-action-denied.json new file mode 100644 index 000000000..6994b5e3f --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/TestData/one-action-denied.json @@ -0,0 +1,190 @@ +{ + "Response": [ + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "write", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "Permit", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": [ + { + "Id": "urn:altinn:obligation:authenticationLevel1", + "AttributeAssignment": [ + { + "AttributeId": "urn:altinn:obligation1-assignment1", + "Value": "2", + "Category": "urn:altinn:minimum-authenticationlevel", + "DataType": "http://www.w3.org/2001/XMLSchema#integer", + "Issuer": null + } + ] + } + ], + "AssociateAdvice": null, + "Category": [ + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:action", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "complete", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + }, + { + "CategoryId": "urn:oasis:names:tc:xacml:3.0:attribute-category:resource", + "Id": null, + "Content": null, + "Attribute": [ + { + "AttributeId": "urn:altinn:instance-id", + "Value": "501337/f38820a7-3b60-45a4-8e41-34103bedc3a6", + "Issuer": null, + "DataType": "http://www.w3.org/2001/XMLSchema#string", + "IncludeInResult": false + } + ] + } + ], + "PolicyIdentifierList": null + }, + { + "Decision": "NotApplicable", + "Status": { + "StatusMessage": null, + "StatusDetails": null, + "StatusCode": { + "Value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "StatusCode": null + } + }, + "Obligations": null, + "AssociateAdvice": null, + "Category": null, + "PolicyIdentifierList": null + } + ] +} diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs index d2667de79..969480088 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs @@ -34,7 +34,7 @@ public async Task CreateInstance_calls_decorated_service_and_update_on_success() // Assert var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCount(1); + diff.Should().HaveCountGreaterOrEqualTo(1); diff.Should().Contain("altinn_app_instances_created{result=\"success\"} 1"); instanceClient.Verify(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())); instanceClient.VerifyNoOtherCalls(); @@ -58,7 +58,7 @@ public async Task CreateInstance_calls_decorated_service_and_update_on_failure() // Assert var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCount(1); + diff.Should().HaveCountGreaterOrEqualTo(1); diff.Should().Contain("altinn_app_instances_created{result=\"failure\"} 1"); instanceClient.Verify(i => i.CreateInstance(It.IsAny(), It.IsAny(), It.IsAny())); instanceClient.VerifyNoOtherCalls(); @@ -78,7 +78,7 @@ public async Task AddCompleteConfirmation_calls_decorated_service_and_update_on_ // Assert var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCount(1); + diff.Should().HaveCountGreaterOrEqualTo(1); diff.Should().Contain("altinn_app_instances_completed{result=\"success\"} 1"); instanceClient.Verify(i => i.AddCompleteConfirmation(It.IsAny(), It.IsAny())); instanceClient.VerifyNoOtherCalls(); @@ -101,7 +101,7 @@ public async Task AddCompleteConfirmation_calls_decorated_service_and_update_on_ // Assert var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCount(1); + diff.Should().HaveCountGreaterOrEqualTo(1); diff.Should().Contain("altinn_app_instances_completed{result=\"failure\"} 1"); instanceClient.Verify(i => i.AddCompleteConfirmation(It.IsAny(), It.IsAny())); instanceClient.VerifyNoOtherCalls(); @@ -121,7 +121,7 @@ public async Task DeleteInstance_calls_decorated_service_and_update_on_success_s // Assert var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCount(1); + diff.Should().HaveCountGreaterOrEqualTo(1); diff.Should().Contain("altinn_app_instances_deleted{result=\"success\",mode=\"soft\"} 1"); instanceClient.Verify(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())); instanceClient.VerifyNoOtherCalls(); @@ -141,7 +141,7 @@ public async Task DeleteInstance_calls_decorated_service_and_update_on_success_s // Assert var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCount(1); + diff.Should().HaveCountGreaterOrEqualTo(1); diff.Should().Contain("altinn_app_instances_deleted{result=\"success\",mode=\"hard\"} 1"); instanceClient.Verify(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())); instanceClient.VerifyNoOtherCalls(); @@ -164,7 +164,7 @@ public async Task DeleteInstance_calls_decorated_service_and_update_on_failure() // Assert var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().HaveCount(1); + diff.Should().HaveCountGreaterOrEqualTo(1); diff.Should().Contain("altinn_app_instances_deleted{result=\"failure\",mode=\"soft\"} 1"); instanceClient.Verify(i => i.DeleteInstance(It.IsAny(), It.IsAny(), It.IsAny())); instanceClient.VerifyNoOtherCalls(); @@ -176,16 +176,12 @@ public async Task GetInstance_calls_decorated_service() // Arrange var instanceClient = new Mock(); var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); // Act var instanceId = Guid.NewGuid(); await instanceClientMetricsDecorator.GetInstance("test-app", "ttd", 1337, instanceId); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().BeEmpty(); instanceClient.Verify(i => i.GetInstance("test-app", "ttd", 1337, instanceId)); instanceClient.VerifyNoOtherCalls(); } @@ -216,15 +212,11 @@ public async Task GetInstances_calls_decorated_service() // Arrange var instanceClient = new Mock(); var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); // Act await instanceClientMetricsDecorator.GetInstances(new Dictionary()); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().BeEmpty(); instanceClient.Verify(i => i.GetInstances(new Dictionary())); instanceClient.VerifyNoOtherCalls(); } @@ -235,16 +227,12 @@ public async Task UpdateProcess_of_instance_owner_calls_decorated_service() // Arrange var instanceClient = new Mock(); var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); var instance = new Instance(); // Act await instanceClientMetricsDecorator.UpdateProcess(instance); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().BeEmpty(); instanceClient.Verify(i => i.UpdateProcess(instance)); instanceClient.VerifyNoOtherCalls(); } @@ -255,16 +243,12 @@ public async Task UpdateReadStatus_of_instance_owner_calls_decorated_service() // Arrange var instanceClient = new Mock(); var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); var instanceGuid = Guid.NewGuid(); // Act await instanceClientMetricsDecorator.UpdateReadStatus(1337, instanceGuid, "read"); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().BeEmpty(); instanceClient.Verify(i => i.UpdateReadStatus(1337, instanceGuid, "read")); instanceClient.VerifyNoOtherCalls(); } @@ -275,17 +259,13 @@ public async Task UpdateSubstatus_of_instance_owner_calls_decorated_service() // Arrange var instanceClient = new Mock(); var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); var instanceGuid = Guid.NewGuid(); var substatus = new Substatus(); // Act await instanceClientMetricsDecorator.UpdateSubstatus(1337, instanceGuid, substatus); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().BeEmpty(); instanceClient.Verify(i => i.UpdateSubstatus(1337, instanceGuid, substatus)); instanceClient.VerifyNoOtherCalls(); } @@ -296,17 +276,13 @@ public async Task UpdatePresentationTexts_of_instance_owner_calls_decorated_serv // Arrange var instanceClient = new Mock(); var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); var instanceGuid = Guid.NewGuid(); var presentationTexts = new PresentationTexts(); // Act await instanceClientMetricsDecorator.UpdatePresentationTexts(1337, instanceGuid, presentationTexts); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().BeEmpty(); instanceClient.Verify(i => i.UpdatePresentationTexts(1337, instanceGuid, presentationTexts)); instanceClient.VerifyNoOtherCalls(); } @@ -317,17 +293,13 @@ public async Task UpdateDataValues_of_instance_owner_calls_decorated_service() // Arrange var instanceClient = new Mock(); var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); var instanceGuid = Guid.NewGuid(); var dataValues = new DataValues(); // Act await instanceClientMetricsDecorator.UpdateDataValues(1337, instanceGuid, dataValues); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().BeEmpty(); instanceClient.Verify(i => i.UpdateDataValues(1337, instanceGuid, dataValues)); instanceClient.VerifyNoOtherCalls(); } diff --git a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs index c0349841a..32f4b08cb 100644 --- a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs @@ -4,8 +4,11 @@ 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.Elements; +using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; using Altinn.App.Core.Models; using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Moq; using Xunit; @@ -19,41 +22,41 @@ public async Task GetPartyList_returns_party_list_from_AuthorizationClient() { // Input int userId = 1337; - + // Arrange Mock authorizationClientMock = new Mock(); List partyList = new List(); authorizationClientMock.Setup(a => a.GetPartyList(userId)).ReturnsAsync(partyList); AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); - + // Act List? result = await authorizationService.GetPartyList(userId); - + // Assert result.Should().BeSameAs(partyList); authorizationClientMock.Verify(a => a.GetPartyList(userId), Times.Once); } - + [Fact] public async Task ValidateSelectedParty_returns_validation_from_AuthorizationClient() { // Input int userId = 1337; int partyId = 1338; - + // Arrange Mock authorizationClientMock = new Mock(); authorizationClientMock.Setup(a => a.ValidateSelectedParty(userId, partyId)).ReturnsAsync(true); AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); - + // Act bool? result = await authorizationService.ValidateSelectedParty(userId, partyId); - + // Assert result.Should().BeTrue(); authorizationClientMock.Verify(a => a.ValidateSelectedParty(userId, partyId), Times.Once); } - + [Fact] public async Task AuthorizeAction_returns_true_when_AutorizationClient_true_and_no_IUserActinAuthorizerProvider_is_provided() { @@ -63,20 +66,20 @@ public async Task AuthorizeAction_returns_true_when_AutorizationClient_true_and_ ClaimsPrincipal user = new ClaimsPrincipal(); string action = "action"; string taskId = "taskId"; - + // Arrange Mock authorizationClientMock = new Mock(); authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); - + // Act bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); - + // Assert result.Should().BeTrue(); authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); } - + [Fact] public async Task AuthorizeAction_returns_false_when_AutorizationClient_false_and_no_IUserActinAuthorizerProvider_is_provided() { @@ -86,20 +89,20 @@ public async Task AuthorizeAction_returns_false_when_AutorizationClient_false_an ClaimsPrincipal user = new ClaimsPrincipal(); string action = "action"; string taskId = "taskId"; - + // Arrange Mock authorizationClientMock = new Mock(); authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(false); AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); - + // Act bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); - + // Assert result.Should().BeFalse(); authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); } - + [Fact] public async Task AuthorizeAction_returns_false_when_AutorizationClient_true_and_one_IUserActinAuthorizerProvider_returns_false() { @@ -109,26 +112,26 @@ public async Task AuthorizeAction_returns_false_when_AutorizationClient_true_and ClaimsPrincipal user = new ClaimsPrincipal(); string action = "action"; string taskId = "taskId"; - + // Arrange Mock authorizationClientMock = new Mock(); authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); - + Mock userActionAuthorizerMock = new Mock(); userActionAuthorizerMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(false); IUserActionAuthorizerProvider userActionAuthorizerProvider = new UserActionAuthorizerProvider("taskId", "action", userActionAuthorizerMock.Object); - + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerProvider }); - + // Act bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); - + // Assert result.Should().BeFalse(); authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); userActionAuthorizerMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); } - + [Fact] public async Task AuthorizeAction_does_not_call_UserActionAuthorizer_if_AuthorizationClient_returns_false() { @@ -138,26 +141,26 @@ public async Task AuthorizeAction_does_not_call_UserActionAuthorizer_if_Authoriz ClaimsPrincipal user = new ClaimsPrincipal(); string action = "action"; string taskId = "taskId"; - + // Arrange Mock authorizationClientMock = new Mock(); authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(false); - + Mock userActionAuthorizerMock = new Mock(); userActionAuthorizerMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); IUserActionAuthorizerProvider userActionAuthorizerProvider = new UserActionAuthorizerProvider("taskId", "action", userActionAuthorizerMock.Object); - + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerProvider }); - + // Act bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); - + // Assert result.Should().BeFalse(); authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); userActionAuthorizerMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Never); } - + [Fact] public async Task AuthorizeAction_calls_all_providers_and_return_true_if_all_true() { @@ -167,30 +170,30 @@ public async Task AuthorizeAction_calls_all_providers_and_return_true_if_all_tru ClaimsPrincipal user = new ClaimsPrincipal(); string action = "action"; string taskId = "taskId"; - + // Arrange Mock authorizationClientMock = new Mock(); authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); - + Mock userActionAuthorizerOneMock = new Mock(); userActionAuthorizerOneMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); IUserActionAuthorizerProvider userActionAuthorizerOneProvider = new UserActionAuthorizerProvider("taskId", "action", userActionAuthorizerOneMock.Object); Mock userActionAuthorizerTwoMock = new Mock(); userActionAuthorizerTwoMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); IUserActionAuthorizerProvider userActionAuthorizerTwoProvider = new UserActionAuthorizerProvider("taskId", "action", userActionAuthorizerTwoMock.Object); - + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerOneProvider, userActionAuthorizerTwoProvider }); - + // Act bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); - + // Assert result.Should().BeTrue(); authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); userActionAuthorizerOneMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); userActionAuthorizerTwoMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); } - + [Fact] public async Task AuthorizeAction_does_not_call_providers_with_non_matching_taskId_and_or_action() { @@ -200,28 +203,28 @@ public async Task AuthorizeAction_does_not_call_providers_with_non_matching_task ClaimsPrincipal user = new ClaimsPrincipal(); string action = "action"; string taskId = "taskId"; - + // Arrange Mock authorizationClientMock = new Mock(); authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); - + Mock userActionAuthorizerOneMock = new Mock(); userActionAuthorizerOneMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(false); IUserActionAuthorizerProvider userActionAuthorizerOneProvider = new UserActionAuthorizerProvider("taskId", "action2", userActionAuthorizerOneMock.Object); - + Mock userActionAuthorizerTwoMock = new Mock(); userActionAuthorizerTwoMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(false); IUserActionAuthorizerProvider userActionAuthorizerTwoProvider = new UserActionAuthorizerProvider("taskId2", "action", userActionAuthorizerTwoMock.Object); - + Mock userActionAuthorizerThreeMock = new Mock(); userActionAuthorizerThreeMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(false); IUserActionAuthorizerProvider userActionAuthorizerThreeProvider = new UserActionAuthorizerProvider("taskId3", "action3", userActionAuthorizerThreeMock.Object); - + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerOneProvider, userActionAuthorizerTwoProvider, userActionAuthorizerThreeProvider }); - + // Act bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); - + // Assert result.Should().BeTrue(); authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); @@ -229,7 +232,7 @@ public async Task AuthorizeAction_does_not_call_providers_with_non_matching_task userActionAuthorizerTwoMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Never); userActionAuthorizerThreeMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Never); } - + [Fact] public async Task AuthorizeAction_calls_providers_with_task_null_and_or_action_null() { @@ -239,28 +242,28 @@ public async Task AuthorizeAction_calls_providers_with_task_null_and_or_action_n ClaimsPrincipal user = new ClaimsPrincipal(); string action = "action"; string taskId = "taskId"; - + // Arrange Mock authorizationClientMock = new Mock(); authorizationClientMock.Setup(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId)).ReturnsAsync(true); - + Mock userActionAuthorizerOneMock = new Mock(); userActionAuthorizerOneMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); IUserActionAuthorizerProvider userActionAuthorizerOneProvider = new UserActionAuthorizerProvider(null, "action", userActionAuthorizerOneMock.Object); - + Mock userActionAuthorizerTwoMock = new Mock(); userActionAuthorizerTwoMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); IUserActionAuthorizerProvider userActionAuthorizerTwoProvider = new UserActionAuthorizerProvider("taskId", null, userActionAuthorizerTwoMock.Object); - + Mock userActionAuthorizerThreeMock = new Mock(); userActionAuthorizerThreeMock.Setup(a => a.AuthorizeAction(It.IsAny())).ReturnsAsync(true); IUserActionAuthorizerProvider userActionAuthorizerThreeProvider = new UserActionAuthorizerProvider(null, null, userActionAuthorizerThreeMock.Object); - + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List() { userActionAuthorizerOneProvider, userActionAuthorizerTwoProvider, userActionAuthorizerThreeProvider }); - + // Actπ bool result = await authorizationService.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId); - + // Assert result.Should().BeTrue(); authorizationClientMock.Verify(a => a.AuthorizeAction(appIdentifier, instanceIdentifier, user, action, taskId), Times.Once); @@ -268,4 +271,68 @@ public async Task AuthorizeAction_calls_providers_with_task_null_and_or_action_n userActionAuthorizerTwoMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); userActionAuthorizerThreeMock.Verify(a => a.AuthorizeAction(It.IsAny()), Times.Once); } + + [Fact] + private async Task AuthorizeActions_returns_list_of_UserActions_with_auth_decisions() + { + // Input + Instance instance = new Instance(); + ClaimsPrincipal user = new ClaimsPrincipal(); + List actions = new List() + { + new AltinnAction("read"), + new AltinnAction("write"), + new AltinnAction("brew-coffee"), + new AltinnAction("drink-coffee", ActionType.UserAction), + }; + var actionsStrings = new List() { "read", "write", "brew-coffee", "drink-coffee" }; + + // Arrange + Mock authorizationClientMock = new Mock(); + authorizationClientMock.Setup(a => a.AuthorizeActions(instance, user, actionsStrings)).ReturnsAsync(new Dictionary() + { + { "read", true }, + { "write", true }, + { "brew-coffee", true }, + { "drink-coffee", false } + }); + + AuthorizationService authorizationService = new AuthorizationService(authorizationClientMock.Object, new List()); + + // Act + List result = await authorizationService.AuthorizeActions(instance, user, actions); + + List expected = new List() + { + new UserAction() + { + Id = "read", + ActionType = ActionType.ProcessAction, + Authorized = true + }, + new UserAction() + { + Id = "write", + ActionType = ActionType.ProcessAction, + Authorized = true + }, + new UserAction() + { + Id = "brew-coffee", + ActionType = ActionType.ProcessAction, + Authorized = true + }, + new UserAction() + { + Id = "drink-coffee", + ActionType = ActionType.UserAction, + Authorized = false + } + }; + + // Assert + result.Should().BeEquivalentTo(expected); + authorizationClientMock.Verify(a => a.AuthorizeActions(instance, user, actionsStrings), Times.Once); + authorizationClientMock.VerifyNoOtherCalls(); + } } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index 144d3cc3e..8b50955c0 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -870,7 +870,7 @@ private IProcessEngine GetProcessEngine(Mock? processReaderMock _profileMock.Object, _processNavigatorMock.Object, _processEventDispatcherMock.Object, - new UserActionFactory(new List())); + new UserActionService(new List())); } public void Dispose() diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs index 4d06f43f1..587fbed58 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessNavigatorTests.cs @@ -60,14 +60,8 @@ public async void GetNextTask_returns_default_if_no_filtering_is_implemented_and TaskType = "confirm", AltinnActions = new() { - new() - { - Value = "confirm" - }, - new() - { - Value = "reject" - } + new("confirm"), + new("reject") } } }, @@ -103,10 +97,7 @@ public async void GetNextTask_runs_custom_filter_and_returns_result() TaskType = "data", AltinnActions = new() { - new() - { - Value = "submit" - } + new("submit") } } }, diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs index 21b92a900..5b7803ab1 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs @@ -313,10 +313,8 @@ public void GetFlowElement_returns_ProcessTask_with_id() { AltinnActions = new List() { - new() - { - Value = "submit", - } + new("submit", ActionType.ProcessAction), + new("lookup", ActionType.UserAction) }, TaskType = "data", SignatureConfiguration = new() diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn index 057b32948..124c6ba6e 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn @@ -18,6 +18,7 @@ submit + lookup data From a3eb2bdd21c8be232a9530faa4ef84ef68950ed1 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Mon, 27 Nov 2023 09:32:06 +0100 Subject: [PATCH 33/46] Fix indent error in codeql.yml --- .github/workflows/codeql.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3e616e95f..6ce02fd3f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,11 +30,11 @@ jobs: # We can remove if using an officially supported .NET version. # See https://github.com/github/codeql-action/issues/757#issuecomment-977546999 - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - include-prerelease: true + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + include-prerelease: true - name: Checkout repository uses: actions/checkout@v4 From ebd99bbb3b66b720f318b1dce8513940c15c925d Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Thu, 30 Nov 2023 08:43:20 +0100 Subject: [PATCH 34/46] Add partyId correctly in test tokens (#367) Also add a Dockerfile for running tests on linux --- .../Controllers/ActionsControllerTests.cs | 16 ++++++++-------- .../EventsReceiverControllerTests.cs | 4 ++-- .../InstancesController_CopyInstanceTests.cs | 12 ++++++------ .../Controllers/StatelessDataControllerTests.cs | 4 ++-- .../Altinn.App.Api.Tests/Utils/PrincipalUtil.cs | 17 +++++++++++------ test/Dockerfile | 17 +++++++++++++++++ 6 files changed, 46 insertions(+), 24 deletions(-) create mode 100644 test/Dockerfile diff --git a/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs index 1321186c6..1a28fd745 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs @@ -27,7 +27,7 @@ public async Task Perform_returns_403_if_user_not_authorized() Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.DeleteInstance(org, app, 1337, guid); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1000, 3); + string token = PrincipalUtil.GetToken(1000, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); // Cleanup testdata @@ -61,7 +61,7 @@ public async Task Perform_returns_401_if_userId_is_null() Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.DeleteInstance(org, app, 1337, guid); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(null, 3); + string token = PrincipalUtil.GetToken(null, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); // Cleanup testdata @@ -79,7 +79,7 @@ public async Task Perform_returns_400_if_action_is_null() Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.DeleteInstance(org, app, 1337, guid); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1000, 3); + string token = PrincipalUtil.GetToken(1000, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":null}", Encoding.UTF8, "application/json")); // Cleanup testdata @@ -97,7 +97,7 @@ public async Task Perform_returns_409_if_process_not_started() Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef43"); TestData.DeleteInstance(org, app, 1337, guid); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1000, 3); + string token = PrincipalUtil.GetToken(1000, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); // Cleanup testdata @@ -115,7 +115,7 @@ public async Task Perform_returns_409_if_process_ended() Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef42"); TestData.DeleteInstance(org, app, 1337, guid); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1000, 3); + string token = PrincipalUtil.GetToken(1000, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); // Cleanup testdata @@ -137,7 +137,7 @@ public async Task Perform_returns_200_if_action_succeeded() Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.DeleteInstance(org, app, 1337, guid); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1000, 3); + string token = PrincipalUtil.GetToken(1000, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); // Cleanup testdata @@ -159,7 +159,7 @@ public async Task Perform_returns_400_if_action_failed() Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.DeleteInstance(org, app, 1337, guid); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1001, 3); + string token = PrincipalUtil.GetToken(1001, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); // Cleanup testdata @@ -181,7 +181,7 @@ public async Task Perform_returns_404_if_action_implementation_not_found() Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.DeleteInstance(org, app, 1337, guid); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1001, 3); + string token = PrincipalUtil.GetToken(1001, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"notfound\"}", Encoding.UTF8, "application/json")); // Cleanup testdata diff --git a/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs index 904a0c05f..73614b011 100644 --- a/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs @@ -28,7 +28,7 @@ public EventsReceiverControllerTests(WebApplicationFactory factory) public async Task Post_ValidEventType_ShouldReturnOk() { var client = _factory.CreateClient(); - string token = PrincipalUtil.GetToken(1337); + string token = PrincipalUtil.GetToken(1337, null); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); CloudEvent cloudEvent = new() { @@ -58,7 +58,7 @@ public async Task Post_ValidEventType_ShouldReturnOk() public async Task Post_NonValidEventType_ShouldReturnBadRequest() { var client = _factory.CreateClient(); - string token = PrincipalUtil.GetToken(1337); + string token = PrincipalUtil.GetToken(1337, null); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); CloudEvent cloudEvent = new() { diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs index 922333917..48c4e1741 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs @@ -153,7 +153,7 @@ public async Task CopyInstance_AsUnauthorized_ReturnsForbidden() // Arrange const string Org = "ttd"; const string AppName = "copy-instance"; - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337)); + _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); _appMetadata.Setup(a => a.GetApplicationMetadata()) .ReturnsAsync(CreateApplicationMetadata($"{Org}/{AppName}", true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) @@ -186,7 +186,7 @@ public async Task CopyInstance_InstanceNotArchived_ReturnsBadRequest() Status = new InstanceStatus() { IsArchived = false } }; - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337)); + _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); _appMetadata.Setup(a => a.GetApplicationMetadata()) .ReturnsAsync(CreateApplicationMetadata($"{Org}/{AppName}", true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) @@ -221,7 +221,7 @@ public async Task CopyInstance_InstanceDoesNotExists_ReturnsBadRequest() PlatformHttpException platformHttpException = await PlatformHttpException.CreateAsync(new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)); - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337)); + _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); _appMetadata.Setup(a => a.GetApplicationMetadata()) .ReturnsAsync(CreateApplicationMetadata($"{Org}/{AppName}", true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) @@ -256,7 +256,7 @@ public async Task CopyInstance_PlatformReturnsError_ThrowsException() PlatformHttpException platformHttpException = await PlatformHttpException.CreateAsync(new HttpResponseMessage(System.Net.HttpStatusCode.BadGateway)); - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337)); + _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); _appMetadata.Setup(a => a.GetApplicationMetadata()) .ReturnsAsync(CreateApplicationMetadata($"{Org}/{AppName}", true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) @@ -300,7 +300,7 @@ public async Task CopyInstance_InstantiationValidationFails_ReturnsForbidden() }; InstantiationValidationResult? instantiationValidationResult = new() { Valid = false }; - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337)); + _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); _appMetadata.Setup(a => a.GetApplicationMetadata()) .ReturnsAsync(CreateApplicationMetadata($"{Org}/{AppName}", true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) @@ -347,7 +347,7 @@ public async Task CopyInstance_EverythingIsFine_ReturnsRedirect() }; InstantiationValidationResult? instantiationValidationResult = new() { Valid = true }; - _httpContextMock.Setup(hc => hc.User).Returns(PrincipalUtil.GetUserPrincipal(1337)); + _httpContextMock.Setup(hc => hc.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); _httpContextMock.Setup(hc => hc.Request).Returns(Mock.Of()); _appMetadata.Setup(a => a.GetApplicationMetadata()) .ReturnsAsync(CreateApplicationMetadata($"{Org}/{AppName}", true)); diff --git a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs index 95cee11c6..30fc7e575 100644 --- a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs @@ -116,7 +116,7 @@ public async void Get_Returns_BadRequest_when_party_header_count_greater_than_on var factory = new StatelessDataControllerWebApplicationFactory(); var client = factory.CreateClient(); - string token = PrincipalUtil.GetToken(1337); + string token = PrincipalUtil.GetToken(1337, null); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var request = new HttpRequestMessage(HttpMethod.Get, "/tdd/demo-app/v1/data?dataType=xml"); request.Headers.Add("party", new string[]{"partyid:234", "partyid:234"}); // Double header @@ -143,7 +143,7 @@ public async void Get_Returns_Forbidden_when_party_has_no_rights() var factory = new StatelessDataControllerWebApplicationFactory(); var client = factory.CreateClient(); - string token = PrincipalUtil.GetToken(1337); + string token = PrincipalUtil.GetToken(1337, null); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var request = new HttpRequestMessage(HttpMethod.Get, "/tdd/demo-app/v1/data?dataType=xml"); request.Headers.Add("party", new string[]{"partyid:234"}); diff --git a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs index a0dde5e9a..94280afcd 100644 --- a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs +++ b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs @@ -7,25 +7,30 @@ namespace Altinn.App.Api.Tests.Utils { public static class PrincipalUtil { - public static string GetToken(int? userId, int authenticationLevel = 2) + public static string GetToken(int? userId, int? partyId, int authenticationLevel = 2) { - ClaimsPrincipal principal = GetUserPrincipal(userId, authenticationLevel); + ClaimsPrincipal principal = GetUserPrincipal(userId, partyId, authenticationLevel); string token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); return token; } - public static ClaimsPrincipal GetUserPrincipal(int? userId, int authenticationLevel = 2) + public static ClaimsPrincipal GetUserPrincipal(int? userId, int? partyId, int authenticationLevel = 2) { List claims = new List(); string issuer = "www.altinn.no"; - if (userId != null) + + claims.Add(new Claim(ClaimTypes.NameIdentifier, $"user-{userId}-{partyId}", ClaimValueTypes.String, issuer)); + if (userId > 0) { - claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString()!, ClaimValueTypes.String, issuer)); claims.Add(new Claim(AltinnCoreClaimTypes.UserId, userId.ToString()!, ClaimValueTypes.String, issuer)); + } + + if (partyId > 0) + { claims.Add(new Claim(AltinnCoreClaimTypes.PartyID, userId.ToString()!, ClaimValueTypes.Integer32, issuer)); } - claims.Add(new Claim(AltinnCoreClaimTypes.UserName, "UserOne", ClaimValueTypes.String, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.UserName, $"User{userId}", ClaimValueTypes.String, issuer)); claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticationLevel, authenticationLevel.ToString(), ClaimValueTypes.Integer32, issuer)); diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 000000000..c9578caa9 --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,17 @@ +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY AppLibDotnet.sln . +COPY test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj test/Altinn.App.Api.Tests/ +COPY test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj test/Altinn.App.Core.Tests/ +COPY src/Altinn.App.Api/Altinn.App.Api.csproj src/Altinn.App.Api/ +COPY src/Altinn.App.Core/Altinn.App.Core.csproj src/Altinn.App.Core/ +RUN dotnet restore + +COPY . . + +RUN dotnet test + +# Run in project root with +# docker build --progress=plain -f test/Dockerfile . \ No newline at end of file From 118723cac06435533dc1f2b93448b4d46ccec3a2 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Thu, 30 Nov 2023 08:44:30 +0100 Subject: [PATCH 35/46] Get rid of Microsoft.AspNet.WebApi.Client that uses Newtonsoft (#363) System.Text.Json includes some bolilerplate and in my opinion wrong defaults. I added JsonSerializerPremissive witch provides dropp inn replacements with more sensible defaults. --- src/Altinn.App.Core/Altinn.App.Core.csproj | 1 - .../Altinn2MetadataApiClient.cs | 4 +- .../Helpers/JsonSerializerPermissive.cs | 49 +++++++++++++++++++ .../Clients/Profile/ProfileClient.cs | 7 +-- .../Clients/Register/AltinnPartyClient.cs | 7 ++- .../Clients/Register/RegisterERClient.cs | 3 +- .../Clients/Storage/TextClient.cs | 3 +- .../Internal/Profile/IProfileClient.cs | 2 +- .../Clients/Storage/SignClientTests.cs | 2 +- 9 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 src/Altinn.App.Core/Helpers/JsonSerializerPermissive.cs diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index 49b80a61f..51bbd3e3c 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -17,7 +17,6 @@ - diff --git a/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2MetadataApiClient.cs b/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2MetadataApiClient.cs index dfb9fafde..4d117789d 100644 --- a/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2MetadataApiClient.cs +++ b/src/Altinn.App.Core/Features/Options/Altinn2Provider/Altinn2MetadataApiClient.cs @@ -1,4 +1,6 @@ #nullable enable +using Altinn.App.Core.Helpers; + namespace Altinn.App.Core.Features.Options.Altinn2Provider { /// @@ -33,7 +35,7 @@ public async Task GetAltinn2Codelist(string id, string response = await _client.GetAsync($"https://www.altinn.no/api/metadata/codelists/{id}/{version?.ToString() ?? string.Empty}"); } response.EnsureSuccessStatusCode(); - var codelist = await response.Content.ReadAsAsync(); + var codelist = await JsonSerializerPermissive.DeserializeAsync(response.Content); return codelist; } } diff --git a/src/Altinn.App.Core/Helpers/JsonSerializerPermissive.cs b/src/Altinn.App.Core/Helpers/JsonSerializerPermissive.cs new file mode 100644 index 000000000..e0603dbfb --- /dev/null +++ b/src/Altinn.App.Core/Helpers/JsonSerializerPermissive.cs @@ -0,0 +1,49 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.Platform.Register.Models; + +namespace Altinn.App.Core.Helpers; + +/// +/// Wrapper of with permissive settings parsing settings. +/// +public static class JsonSerializerPermissive +{ + /// + /// for the most permissive parsing of JSON. + /// + public static readonly JsonSerializerOptions JsonSerializerOptionsDefaults = new(JsonSerializerDefaults.Web) + { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + Converters = + { + new JsonStringEnumConverter(), + }, + }; + + /// + /// Simple wrapper of with permissive defaults. + /// + public static T Deserialize(string content) + { + return JsonSerializer.Deserialize(content, JsonSerializerOptionsDefaults) ?? throw new JsonException("Could not deserialize json value \"null\" to type " + typeof(T).FullName); + } + + /// + /// Simple wrapper of with permissive defaults. + /// + public static async Task DeserializeAsync(HttpContent content, CancellationToken cancellationToken = default) + { + await using var stream = await content.ReadAsStreamAsync(cancellationToken); + return await JsonSerializer.DeserializeAsync(stream, JsonSerializerOptionsDefaults, cancellationToken) ?? throw new JsonException("Could not deserialize json value \"null\" to type " + typeof(T).FullName); + } + + /// + /// Simple wrapper of with permissive defaults. + /// + public static string Serialize(PartyLookup partyLookup) + { + return JsonSerializer.Serialize(partyLookup, JsonSerializerOptionsDefaults); + } +} diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs index a0eb7ea1f..65e081f14 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClient.cs @@ -2,6 +2,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Models; @@ -57,9 +58,9 @@ public ProfileClient( } /// - public async Task GetUserProfile(int userId) + public async Task GetUserProfile(int userId) { - UserProfile userProfile = null; + UserProfile? userProfile = null; string endpointUrl = $"users/{userId}"; string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); @@ -68,7 +69,7 @@ public async Task GetUserProfile(int userId) HttpResponseMessage response = await _client.GetAsync(token, endpointUrl, _accessTokenGenerator.GenerateAccessToken(applicationMetadata.Org, applicationMetadata.AppIdentifier.App)); if (response.StatusCode == System.Net.HttpStatusCode.OK) { - userProfile = await response.Content.ReadAsAsync(); + userProfile = await JsonSerializerPermissive.DeserializeAsync(response.Content); } else { diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs index fece3e574..22542922f 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Newtonsoft.Json; namespace Altinn.App.Core.Infrastructure.Clients.Register { @@ -70,7 +69,7 @@ public AltinnPartyClient( HttpResponseMessage response = await _client.GetAsync(token, endpointUrl, _accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App)); if (response.StatusCode == HttpStatusCode.OK) { - party = await response.Content.ReadAsAsync(); + party = await JsonSerializerPermissive.DeserializeAsync(response.Content); } else if (response.StatusCode == HttpStatusCode.Unauthorized) { @@ -92,7 +91,7 @@ public async Task LookupParty(PartyLookup partyLookup) string endpointUrl = "parties/lookup"; string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); - StringContent content = new StringContent(JsonConvert.SerializeObject(partyLookup)); + StringContent content = new StringContent(JsonSerializerPermissive.Serialize(partyLookup)); content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); HttpRequestMessage request = new HttpRequestMessage { @@ -108,7 +107,7 @@ public async Task LookupParty(PartyLookup partyLookup) HttpResponseMessage response = await _client.SendAsync(request); if (response.StatusCode == HttpStatusCode.OK) { - party = await response.Content.ReadAsAsync(); + party = await JsonSerializerPermissive.DeserializeAsync(response.Content); } else { diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs index 0b21614b0..6e1ca9048 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Register/RegisterERClient.cs @@ -2,6 +2,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Registers; using Altinn.App.Core.Models; @@ -69,7 +70,7 @@ public RegisterERClient( if (response.StatusCode == System.Net.HttpStatusCode.OK) { - organization = await response.Content.ReadAsAsync(); + organization = await JsonSerializerPermissive.DeserializeAsync(response.Content); } else { diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs index 911f38e8c..21519e87f 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/TextClient.cs @@ -2,6 +2,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Texts; using Altinn.Platform.Storage.Interface.Models; using AltinnCore.Authentication.Utils; @@ -73,7 +74,7 @@ public TextClient( HttpResponseMessage response = await _client.GetAsync(token, url); if (response.StatusCode == System.Net.HttpStatusCode.OK) { - textResource = await response.Content.ReadAsAsync(); + textResource = await JsonSerializerPermissive.DeserializeAsync(response.Content); _memoryCache.Set(cacheKey, textResource, cacheEntryOptions); } else diff --git a/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs b/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs index d6e5cf181..17206dd2a 100644 --- a/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs +++ b/src/Altinn.App.Core/Internal/Profile/IProfileClient.cs @@ -12,6 +12,6 @@ public interface IProfileClient /// /// the user id /// The userprofile for the given user id - Task GetUserProfile(int userId); + Task GetUserProfile(int userId); } } diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs index 81a80bb9b..a16408a41 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/SignClientTests.cs @@ -89,7 +89,7 @@ public async Task SignDataElements_sends_request_to_platform() platformRequest.Should().NotBeNull(); platformRequest!.Method.Should().Be(HttpMethod.Post); platformRequest!.RequestUri!.ToString().Should().Be($"{apiStorageEndpoint}instances/{instanceIdentifier.InstanceOwnerPartyId}/{instanceIdentifier.InstanceGuid}/sign"); - SignRequest actual = await platformRequest.Content.ReadAsAsync(); + SignRequest actual = await JsonSerializerPermissive.DeserializeAsync(platformRequest!.Content!); actual.Should().BeEquivalentTo(expectedRequest); } From a0886626e76773e2735c7b11aeb8ff06a2bf4400 Mon Sep 17 00:00:00 2001 From: Mikael Solstad Date: Thu, 30 Nov 2023 09:59:58 +0100 Subject: [PATCH 36/46] Feat/process tasks (#353) * feat: add ProcessTasks to AppProcessState * wip: add test for process controller * feat: add ProcessTasks to AppProcessState * wip: add test for process controller * Adding first test for processcontroller * Fix bad case for roles folder in testsetup * User ArgumentNullException.ThrowIfNull instead of custom method * Fix codesmells --------- Co-authored-by: Vemund Gaukstad --- .../Controllers/ProcessController.cs | 12 +++ .../Process/Elements/AppProcessState.cs | 9 +- .../Elements/AppProcessTaskTypeInfo.cs | 25 +++++ .../Internal/Process/IProcessReader.cs | 6 +- .../Internal/Process/ProcessReader.cs | 13 +-- .../Controllers/ProcessControllerTests.cs | 96 +++++++++++++++++++ ...b-83ed-44df-85a7-2f104c640bff.pretest.json | 39 ++++++++ test/Altinn.App.Api.Tests/Data/TestData.cs | 2 +- .../config/process/process.bpmn | 9 +- .../Mocks/AuthorizationMock.cs | 18 +++- .../InstanceClientMetricsDecoratorTests.cs | 6 +- 11 files changed, 213 insertions(+), 22 deletions(-) create mode 100644 src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs create mode 100644 test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500000/5d9e906b-83ed-44df-85a7-2f104c640bff.pretest.json diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index d853bbf10..d207a2560 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -474,6 +474,18 @@ private async Task ConvertAndAuthorizeActions(Instance instance appProcessState.CurrentTask.UserActions = authDecisions; } } + + var processTasks = new List(); + foreach (var processElement in _processReader.GetAllFlowElements().OfType()) + { + processTasks.Add(new AppProcessTaskTypeInfo + { + ElementId = processElement.Id, + AltinnTaskType = processElement.ExtensionElements?.TaskExtension?.TaskType + }); + } + + appProcessState.ProcessTasks = processTasks; return appProcessState; } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessState.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessState.cs index ecfc689c1..eac09508a 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessState.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessState.cs @@ -26,7 +26,6 @@ public AppProcessState(ProcessState? processState) { return; } - Started = processState.Started; StartEvent = processState.StartEvent; if (processState.CurrentTask != null) @@ -35,10 +34,16 @@ public AppProcessState(ProcessState? processState) } Ended = processState.Ended; EndEvent = processState.EndEvent; + } - /// /// Gets or sets a status object containing the task info of the currentTask of an ongoing process. /// public new AppProcessElementInfo? CurrentTask { get; set; } + + /// + /// Gets or sets a list of all tasks. The list contains information about the task Id + /// and the task type. + /// + public List? ProcessTasks { get; set; } } diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs new file mode 100644 index 000000000..179af8761 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessTaskTypeInfo.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.Xml.Serialization; +using System.Text.Json.Serialization; + +namespace Altinn.App.Core.Internal.Process.Elements; + +/// +/// Representation of a task's id and type. Used by the frontend to determine which tasks +/// exist, and their type. +/// +public class AppProcessTaskTypeInfo +{ + /// + /// Gets or sets the task type + /// + [XmlElement("altinnTaskType", Namespace = "http://altinn.no/process")] + public string? AltinnTaskType { get; set; } + + + /// + /// Gets or sets a reference to the current task/event element id as given in the process definition. + /// + [JsonPropertyName(name: "elementId")] + public string? ElementId { get; set; } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Internal/Process/IProcessReader.cs b/src/Altinn.App.Core/Internal/Process/IProcessReader.cs index 52c281707..492688071 100644 --- a/src/Altinn.App.Core/Internal/Process/IProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/IProcessReader.cs @@ -8,7 +8,6 @@ namespace Altinn.App.Core.Internal.Process; /// public interface IProcessReader { - /// /// Get all defined StartEvents in the process /// @@ -111,4 +110,9 @@ public interface IProcessReader /// or null public ProcessElement? GetFlowElement(string? elementId); + /// + /// Returns all available ProcessElements + /// + /// + public List GetAllFlowElements(); } diff --git a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs index 88c17beeb..0d2b3d9fb 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessReader.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessReader.cs @@ -105,7 +105,7 @@ public List GetSequenceFlowIds() /// public ProcessElement? GetFlowElement(string? elementId) { - EnsureArgumentNotNull(elementId, nameof(elementId)); + ArgumentNullException.ThrowIfNull(elementId); ProcessTask? task = _definitions.Process.Tasks.Find(t => t.Id == elementId); if (task != null) @@ -131,7 +131,7 @@ public List GetSequenceFlowIds() /// public List GetNextElements(string? currentElementId) { - EnsureArgumentNotNull(currentElementId, nameof(currentElementId)); + ArgumentNullException.ThrowIfNull(currentElementId); List nextElements = new List(); List allElements = GetAllFlowElements(); if (!allElements.Exists(e => e.Id == currentElementId)) @@ -158,13 +158,8 @@ public List GetOutgoingSequenceFlows(ProcessElement? flowElement) return GetSequenceFlows().FindAll(sf => flowElement.Outgoing.Contains(sf.Id)).ToList(); } - private static void EnsureArgumentNotNull(object? argument, string paramName) - { - if (argument == null) - throw new ArgumentNullException(paramName); - } - - private List GetAllFlowElements() + /// + public List GetAllFlowElements() { List flowElements = new List(); flowElements.AddRange(GetStartEvents()); diff --git a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs new file mode 100644 index 000000000..9cb282b8b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs @@ -0,0 +1,96 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json; +using Altinn.App.Api.Models; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Api.Tests.Utils; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace Altinn.App.Api.Tests.Controllers; + +public class ProcessControllerTests : ApiTestBase, IClassFixture> +{ + public ProcessControllerTests(WebApplicationFactory factory) : base(factory) + { + } + + [Fact] + public async Task Get_ShouldReturnProcessTasks() + { + string org = "tdd"; + string app = "contributer-restriction"; + int partyId = 500000; + Guid instanceId = new Guid("5d9e906b-83ed-44df-85a7-2f104c640bff"); + HttpClient client = GetRootedClient(org, app); + + TestData.DeleteInstance(org, app, partyId, instanceId); + TestData.PrepareInstance(org, app, partyId, instanceId); + + string token = PrincipalUtil.GetToken(1337, 500000, 3); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + string url = $"/{org}/{app}/instances/{partyId}/{instanceId}/process"; + HttpResponseMessage response = await client.GetAsync(url); + TestData.DeleteInstance(org, app, partyId, instanceId); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var content = await response.Content.ReadAsStringAsync(); + var expectedString = """ + { + "currentTask": { + "actions": { + "read": true, + "write": true + }, + "userActions": [ + { + "id": "read", + "authorized": true, + "type": "ProcessAction" + }, + { + "id": "write", + "authorized": true, + "type": "ProcessAction" + } + ], + "read": true, + "write": true, + "flow": 2, + "started": "2019-12-05T13:24:34.9196661Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "ended": null, + "validated": { + "timestamp": "2020-02-07T10:46:36.985894+01:00", + "canCompleteTask": false + }, + "flowType": null + }, + "processTasks": [ + { + "altinnTaskType": "data", + "elementId": "Task_1" + } + ], + "started": "2019-12-05T13:24:34.8412179Z", + "startEvent": "StartEvent_1", + "ended": null, + "endEvent": null + } + """; + CompareResult(expectedString, content); + } + + + //TODO: replace this assertion with a proper one once fluentassertions has a json compare feature scheduled for v7 https://github.com/fluentassertions/fluentassertions/issues/2205 + private static void CompareResult(string expectedString, string actualString) + { + T? expected = JsonSerializer.Deserialize(expectedString); + T? actual = JsonSerializer.Deserialize(actualString); + actual.Should().BeEquivalentTo(expected); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500000/5d9e906b-83ed-44df-85a7-2f104c640bff.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500000/5d9e906b-83ed-44df-85a7-2f104c640bff.pretest.json new file mode 100644 index 000000000..69a5be102 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500000/5d9e906b-83ed-44df-85a7-2f104c640bff.pretest.json @@ -0,0 +1,39 @@ +{ + "id": "500000/5d9e906b-83ed-44df-85a7-2f104c640bff", + "instanceOwner": { + "partyId": "500000", + "personNumber": "01039012345" + }, + "appId": "tdd/contributer-restriction", + "org": "tdd", + "process": { + "started": "2019-12-05T13:24:34.8412179Z", + "startEvent": "StartEvent_1", + "currentTask": { + "flow": 2, + "started": "2019-12-05T13:24:34.9196661Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "validated": { + "timestamp": "2020-02-07T10:46:36.985894+01:00", + "canCompleteTask": false + } + } + }, + "status": { + "isArchived": false, + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + }, + "data": [ + { + "id": "de288942-a8af-4f77-a1f1-6e1ede1cd502", + "dataType": "default", + "contentType": "application/xml", + "size": 0, + "locked": false + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/TestData.cs b/test/Altinn.App.Api.Tests/Data/TestData.cs index d7f7acca8..1f74ef50c 100644 --- a/test/Altinn.App.Api.Tests/Data/TestData.cs +++ b/test/Altinn.App.Api.Tests/Data/TestData.cs @@ -64,7 +64,7 @@ public static string GetDataBlobPath(string org, string app, int instanceOwnerId public static string GetTestDataRolesFolder(int userId, int resourcePartyId) { string testDataDirectory = GetTestDataRootDirectory(); - return Path.Combine(testDataDirectory, @"authorization/Roles/User_" + userId, "party_" + resourcePartyId, "roles.json"); + return Path.Combine(testDataDirectory, "authorization","roles", "User_" + userId, "party_" + resourcePartyId, "roles.json"); } public static string GetAltinnAppsPolicyPath(string org, string app) diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/process/process.bpmn b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/process/process.bpmn index f28219543..2274ca316 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/process/process.bpmn +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/process/process.bpmn @@ -1,7 +1,7 @@ SequenceFlow_1n56yn5 - + SequenceFlow_1n56yn5 SequenceFlow_1oot28q + + + data + + SequenceFlow_1oot28q diff --git a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs index 43ce27de8..bc843ee9f 100644 --- a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs @@ -41,9 +41,23 @@ public async Task AuthorizeAction(AppIdentifier appIdentifier, InstanceIde return true; } - public Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions) + public async Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions) { - throw new NotImplementedException(); + await Task.CompletedTask; + Dictionary authorizedActions = new Dictionary(); + foreach (var action in actions) + { + if(action.EndsWith("_unauthorized")) + { + authorizedActions.Add(action, false); + } + else + { + authorizedActions.Add(action, true); + } + } + + return authorizedActions; } } } diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs index 969480088..b886f645d 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/InstanceClientMetricsDecoratorTests.cs @@ -10,7 +10,7 @@ using Prometheus; using Xunit; -namespace Altinn.App.Core.Tests.InfrastrucZture.Clients.Storage; +namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage; public class InstanceClientMetricsDecoratorTests { @@ -192,16 +192,12 @@ public async Task GetInstance_instance_calls_decorated_service() // Arrange var instanceClient = new Mock(); var instanceClientMetricsDecorator = new InstanceClientMetricsDecorator(instanceClient.Object); - var preUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); var instance = new Instance(); // Act await instanceClientMetricsDecorator.GetInstance(instance); - var postUpdateMetrics = await PrometheusTestHelper.ReadPrometheusMetricsToString(); // Assert - var diff = GetDiff(preUpdateMetrics, postUpdateMetrics); - diff.Should().BeEmpty(); instanceClient.Verify(i => i.GetInstance(instance)); instanceClient.VerifyNoOtherCalls(); } From ee9c4863784fa3850876ec9c07dd0c62763c2163 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Fri, 8 Dec 2023 11:21:47 +0100 Subject: [PATCH 37/46] Upgrade to dotnet 8 (#373) Upgrade TargetFramework to net8 and dockerfile Make code rewrite more reliable by moving it before the csproj rewrite --- cli-tools/altinn-app-cli/Program.cs | 50 ++++++++++++++----- .../altinn-app-cli/altinn-app-cli.csproj | 4 +- .../DockerfileRewriters/DockerfileRewriter.cs | 50 +++++++++++++++++++ .../Extensions/StringDockerTagExtensions.cs | 33 ++++++++++++ .../ProjectRewriters/ProjectFileRewriter.cs | 14 +++++- 5 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/DockerfileRewriter.cs create mode 100644 cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/Extensions/StringDockerTagExtensions.cs diff --git a/cli-tools/altinn-app-cli/Program.cs b/cli-tools/altinn-app-cli/Program.cs index 603d0bfc3..08b341c55 100644 --- a/cli-tools/altinn-app-cli/Program.cs +++ b/cli-tools/altinn-app-cli/Program.cs @@ -3,6 +3,7 @@ using System.Reflection; using altinn_app_cli.v7Tov8.AppSettingsRewriter; using altinn_app_cli.v7Tov8.CodeRewriters; +using altinn_app_cli.v7Tov8.DockerfileRewriters; using altinn_app_cli.v7Tov8.ProcessRewriter; using altinn_app_cli.v7Tov8.ProjectChecks; using altinn_app_cli.v7Tov8.ProjectRewriters; @@ -21,8 +22,10 @@ static async Task Main(string[] args) var projectFileOption = new Option(name: "--project", description: "The project file to read relative to --folder", getDefaultValue: () => "App/App.csproj"); var processFileOption = new Option(name: "--process", description: "The process file to read relative to --folder", getDefaultValue: () => "App/config/process/process.bpmn"); var appSettingsFolderOption = new Option(name: "--appsettings-folder", description: "The folder where the appsettings.*.json files are located", getDefaultValue: () => "App"); - var targetVersionOption = new Option(name: "--target-version", description: "The target version to upgrade to", getDefaultValue: () => "8.0.0-preview.10"); + var targetVersionOption = new Option(name: "--target-version", description: "The target version to upgrade to", getDefaultValue: () => "8.0.0-preview.11"); + var targetFrameworkOption = new Option(name: "--target-framework", description: "The target dotnet framework version to upgrade to", getDefaultValue: () => "net8.0"); var skipCsprojUpgradeOption = new Option(name: "--skip-csproj-upgrade", description: "Skip csproj file upgrade", getDefaultValue: () => false); + var skipDockerUpgradeOption = new Option(name: "--skip-dockerfile-upgrade", description: "Skip Dockerfile upgrade", getDefaultValue: () => false); var skipCodeUpgradeOption = new Option(name: "--skip-code-upgrade", description: "Skip code upgrade", getDefaultValue: () => false); var skipProcessUpgradeOption = new Option(name: "--skip-process-upgrade", description: "Skip process file upgrade", getDefaultValue: () => false); var skipAppSettingsUpgradeOption = new Option(name: "--skip-appsettings-upgrade", description: "Skip appsettings file upgrade", getDefaultValue: () => false); @@ -34,7 +37,9 @@ static async Task Main(string[] args) processFileOption, appSettingsFolderOption, targetVersionOption, + targetFrameworkOption, skipCsprojUpgradeOption, + skipDockerUpgradeOption, skipCodeUpgradeOption, skipProcessUpgradeOption, skipAppSettingsUpgradeOption, @@ -51,10 +56,12 @@ static async Task Main(string[] args) var processFile = context.ParseResult.GetValueForOption(processFileOption)!; var appSettingsFolder = context.ParseResult.GetValueForOption(appSettingsFolderOption)!; var targetVersion = context.ParseResult.GetValueForOption(targetVersionOption)!; - var skipCodeUpgrade = context.ParseResult.GetValueForOption(skipCodeUpgradeOption)!; - var skipProcessUpgrade = context.ParseResult.GetValueForOption(skipProcessUpgradeOption)!; - var skipCsprojUpgrade = context.ParseResult.GetValueForOption(skipCsprojUpgradeOption)!; - var skipAppSettingsUpgrade = context.ParseResult.GetValueForOption(skipAppSettingsUpgradeOption)!; + var targetFramework = context.ParseResult.GetValueForOption(targetFrameworkOption)!; + var skipCodeUpgrade = context.ParseResult.GetValueForOption(skipCodeUpgradeOption); + var skipProcessUpgrade = context.ParseResult.GetValueForOption(skipProcessUpgradeOption); + var skipCsprojUpgrade = context.ParseResult.GetValueForOption(skipCsprojUpgradeOption); + var skipDockerUpgrade = context.ParseResult.GetValueForOption(skipDockerUpgradeOption); + var skipAppSettingsUpgrade = context.ParseResult.GetValueForOption(skipAppSettingsUpgradeOption); if (projectFolder == "CurrentDirectory") { @@ -97,14 +104,19 @@ static async Task Main(string[] args) return; } - if (!skipCsprojUpgrade) + if (!skipCodeUpgrade) { - returnCode = await UpgradeNugetVersions(projectFile, targetVersion); + returnCode = await UpgradeCode(projectFile); } - - if (!skipCodeUpgrade && returnCode == 0) + + if (!skipCsprojUpgrade && returnCode == 0) { - returnCode = await UpgradeCode(projectFile); + returnCode = await UpgradeProjectFile(projectFile, targetVersion, targetFramework); + } + + if (!skipDockerUpgrade && returnCode == 0) + { + returnCode = await UpgradeDockerfile(Path.Combine(projectFolder, "Dockerfile"), targetFramework); } if (!skipProcessUpgrade && returnCode == 0) @@ -137,7 +149,7 @@ static async Task Main(string[] args) return returnCode; } - static async Task UpgradeNugetVersions(string projectFile, string targetVersion) + static async Task UpgradeProjectFile(string projectFile, string targetVersion, string targetFramework) { if (!File.Exists(projectFile)) { @@ -146,12 +158,26 @@ static async Task UpgradeNugetVersions(string projectFile, string targetVer } Console.WriteLine("Trying to upgrade nuget versions in project file"); - var rewriter = new ProjectFileRewriter(projectFile, targetVersion); + var rewriter = new ProjectFileRewriter(projectFile, targetVersion, targetFramework); await rewriter.Upgrade(); Console.WriteLine("Nuget versions upgraded"); return 0; } + static async Task UpgradeDockerfile(string dockerFile, string targetFramework) + { + if (!File.Exists(dockerFile)) + { + Console.WriteLine($"Dockerfile {dockerFile} does not exist. Please supply location of project with --dockerfile [path/to/Dockerfile]"); + return 1; + } + Console.WriteLine("Trying to upgrade dockerfile"); + var rewriter = new DockerfileRewriter(dockerFile, targetFramework); + await rewriter.Upgrade(); + Console.WriteLine("Dockerfile upgraded"); + return 0; + } + static async Task UpgradeCode(string projectFile) { if (!File.Exists(projectFile)) diff --git a/cli-tools/altinn-app-cli/altinn-app-cli.csproj b/cli-tools/altinn-app-cli/altinn-app-cli.csproj index 7c87512e8..0b29a106f 100644 --- a/cli-tools/altinn-app-cli/altinn-app-cli.csproj +++ b/cli-tools/altinn-app-cli/altinn-app-cli.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 altinn_app_cli latest enable @@ -31,7 +31,7 @@ preview.0 altinn-app-cli true - 10.0 + 12.0 diff --git a/cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/DockerfileRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/DockerfileRewriter.cs new file mode 100644 index 000000000..6316d3f5a --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/DockerfileRewriter.cs @@ -0,0 +1,50 @@ +using System.Text.RegularExpressions; +using altinn_app_cli.v7Tov8.DockerfileRewriters.Extensions; + +namespace altinn_app_cli.v7Tov8.DockerfileRewriters; + +/// +/// Rewrites the dockerfile +/// +public class DockerfileRewriter +{ + private readonly string dockerFilePath; + private readonly string targetFramework; + + /// + /// Creates a new instance of the class + /// + /// + /// + public DockerfileRewriter(string dockerFilePath, string targetFramework = "net8.0") + { + this.dockerFilePath = dockerFilePath; + this.targetFramework = targetFramework; + } + + /// + /// Upgrades the dockerfile + /// + public async Task Upgrade() + { + var dockerFile = await File.ReadAllLinesAsync(dockerFilePath); + var newDockerFile = new List(); + foreach (var line in dockerFile) + { + var imageTag = GetImageTagFromFrameworkVersion(targetFramework); + newDockerFile.Add(line.ReplaceSdkVersion(imageTag).ReplaceAspNetVersion(imageTag)); + } + + await File.WriteAllLinesAsync(dockerFilePath, newDockerFile); + } + + private static string GetImageTagFromFrameworkVersion(string targetFramework) + { + return targetFramework switch + { + "net6.0" => "6.0-alpine", + "net7.0" => "7.0-alpine", + _ => "8.0-alpine" + }; + } +} \ No newline at end of file diff --git a/cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/Extensions/StringDockerTagExtensions.cs b/cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/Extensions/StringDockerTagExtensions.cs new file mode 100644 index 000000000..8f62aea74 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/DockerfileRewriters/Extensions/StringDockerTagExtensions.cs @@ -0,0 +1,33 @@ +using System.Text.RegularExpressions; + +namespace altinn_app_cli.v7Tov8.DockerfileRewriters.Extensions; + +/// +/// Extensions for string replacing tags in dockerfiles +/// +public static class DockerfileStringExtensions +{ + /// + /// Replaces the dotnet sdk image tag version in a dockerfile + /// + /// a line in the dockerfile + /// the new image tag + /// + public static string ReplaceSdkVersion(this string line, string imageTag) + { + const string pattern = @"(^FROM mcr.microsoft.com/dotnet/sdk):(.+?)( AS .*)?$"; + return Regex.Replace(line, pattern, $"$1:{imageTag}$3"); + } + + /// + /// Replaces the aspnet image tag version in a dockerfile + /// + /// a line in the dockerfile + /// the new image tag + /// + public static string ReplaceAspNetVersion(this string line, string imageTag) + { + const string pattern = @"(^FROM mcr.microsoft.com/dotnet/aspnet):(.+?)( AS .*)?$"; + return Regex.Replace(line, pattern, $"$1:{imageTag}$3"); + } +} \ No newline at end of file diff --git a/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs index 11def034d..614d7a491 100644 --- a/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs +++ b/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs @@ -9,13 +9,15 @@ public class ProjectFileRewriter private XDocument doc; private readonly string projectFilePath; private readonly string targetVersion; + private readonly string targetFramework; - public ProjectFileRewriter(string projectFilePath, string targetVersion = "8.0.0") + public ProjectFileRewriter(string projectFilePath, string targetVersion = "8.0.0", string targetFramework = "net8.0") { this.projectFilePath = projectFilePath; this.targetVersion = targetVersion; var xmlString = File.ReadAllText(projectFilePath); doc = XDocument.Parse(xmlString); + this.targetFramework = targetFramework; } public async Task Upgrade() @@ -26,8 +28,10 @@ public async Task Upgrade() { altinnAppCoreElements.ForEach(c => c.Attribute("Version")?.SetValue(targetVersion)); altinnAppApiElements.ForEach(a => a.Attribute("Version")?.SetValue(targetVersion)); - await Save(); } + + GetTargetFrameworkElement()?.ForEach(t => t.SetValue(targetFramework)); + await Save(); } private List? GetAltinnAppCoreElement() @@ -39,6 +43,11 @@ public async Task Upgrade() { return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Api").ToList(); } + + private List? GetTargetFrameworkElement() + { + return doc.Root?.Elements("PropertyGroup").Elements("TargetFramework").ToList(); + } private async Task Save() { @@ -49,5 +58,6 @@ private async Task Save() xws.Encoding = Encoding.UTF8; await using XmlWriter xw = XmlWriter.Create(projectFilePath, xws); await doc.WriteToAsync(xw, CancellationToken.None); + await xw.FlushAsync(); } } From 7cc841eac4d7b834894779591a266a24f251aa68 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Fri, 8 Dec 2023 12:01:50 +0100 Subject: [PATCH 38/46] =?UTF-8?q?Rename=20Action.Type=20from=20userAction?= =?UTF-8?q?=20to=20serverAction.=20FrontendAction=20re=E2=80=A6=20(#376)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename Action.Type from userAction to serverAction. FrontendAction renamed to ClientAction * Add test to better cover response from actionscontroller * Fix codesmells and notices --- .../Controllers/ActionsController.cs | 4 +- .../Models/UserActionResponse.cs | 6 +- .../AltinnExtensionProperties/AltinnAction.cs | 6 +- .../{FrontendAction.cs => ClientAction.cs} | 10 +- .../Models/UserAction/UserActionResult.cs | 16 +-- .../Controllers/ActionsControllerTests.cs | 108 +++++++++++------- .../task-action/config/process/process.bpmn | 2 +- .../Features/Action/UserActionServiceTests.cs | 2 +- .../Auth/AuthorizationServiceTests.cs | 4 +- .../Internal/Process/ProcessReaderTests.cs | 2 +- .../TestData/simple-gateway-default.bpmn | 2 +- 11 files changed, 94 insertions(+), 68 deletions(-) rename src/Altinn.App.Core/Models/UserAction/{FrontendAction.cs => ClientAction.cs} (72%) diff --git a/src/Altinn.App.Api/Controllers/ActionsController.cs b/src/Altinn.App.Api/Controllers/ActionsController.cs index be56f0128..a83a4e888 100644 --- a/src/Altinn.App.Api/Controllers/ActionsController.cs +++ b/src/Altinn.App.Api/Controllers/ActionsController.cs @@ -115,14 +115,14 @@ public async Task> Perform( { return new BadRequestObjectResult(new UserActionResponse() { - FrontendActions = result.FrontendActions, + ClientActions = result.ClientActions, Error = result.Error }); } return new OkObjectResult(new UserActionResponse() { - FrontendActions = result.FrontendActions, + ClientActions = result.ClientActions, UpdatedDataModels = result.UpdatedDataModels }); } diff --git a/src/Altinn.App.Api/Models/UserActionResponse.cs b/src/Altinn.App.Api/Models/UserActionResponse.cs index e05370d38..ee5b315f1 100644 --- a/src/Altinn.App.Api/Models/UserActionResponse.cs +++ b/src/Altinn.App.Api/Models/UserActionResponse.cs @@ -16,10 +16,10 @@ public class UserActionResponse public Dictionary? UpdatedDataModels { get; set; } /// - /// Actions frontend should perform after action has been performed backend + /// Actions the client should perform after action has been performed backend /// - [JsonPropertyName("frontendActions")] - public List? FrontendActions { get; set; } + [JsonPropertyName("clientActions")] + public List? ClientActions { get; set; } /// /// Validation issues that occured when processing action diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs index dce18cd68..9a10e1427 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs @@ -62,9 +62,9 @@ public enum ActionType [XmlEnum("processAction")] ProcessAction, /// - /// The action is a user action + /// The action is a generic server action /// - [XmlEnum("userAction")] - UserAction + [XmlEnum("serverAction")] + ServerAction } } diff --git a/src/Altinn.App.Core/Models/UserAction/FrontendAction.cs b/src/Altinn.App.Core/Models/UserAction/ClientAction.cs similarity index 72% rename from src/Altinn.App.Core/Models/UserAction/FrontendAction.cs rename to src/Altinn.App.Core/Models/UserAction/ClientAction.cs index d1743122b..48ec5c976 100644 --- a/src/Altinn.App.Core/Models/UserAction/FrontendAction.cs +++ b/src/Altinn.App.Core/Models/UserAction/ClientAction.cs @@ -3,9 +3,9 @@ namespace Altinn.App.Core.Models.UserAction; /// -/// Defines an action that should be performed by frontend +/// Defines an action that should be performed by the client /// -public class FrontendAction +public class ClientAction { /// /// Name of the action @@ -20,12 +20,12 @@ public class FrontendAction public Dictionary? Metadata { get; set; } /// - /// Creates a nextPage frontend action + /// Creates a nextPage client action /// /// - public static FrontendAction NextPage() + public static ClientAction NextPage() { - var frontendAction = new FrontendAction() + var frontendAction = new ClientAction() { Name = "nextPage" }; diff --git a/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs b/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs index 02d9605aa..1ffeba500 100644 --- a/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs +++ b/src/Altinn.App.Core/Models/UserAction/UserActionResult.cs @@ -20,9 +20,9 @@ public class UserActionResult public Dictionary? UpdatedDataModels { get; set; } /// - /// Actions for the frontend to perform after the user action has been handled + /// Actions for the client to perform after the user action has been handled /// - public List? FrontendActions { get; set; } + public List? ClientActions { get; set; } /// /// Validation issues that should be displayed to the user @@ -32,14 +32,14 @@ public class UserActionResult /// /// Creates a success result /// - /// + /// /// - public static UserActionResult SuccessResult(List? frontendActions = null) + public static UserActionResult SuccessResult(List? clientActions = null) { var userActionResult = new UserActionResult { Success = true, - FrontendActions = frontendActions + ClientActions = clientActions }; return userActionResult; } @@ -48,14 +48,14 @@ public static UserActionResult SuccessResult(List? frontendActio /// Creates a failure result /// /// - /// + /// /// - public static UserActionResult FailureResult(ActionError error, List? frontendActions = null) + public static UserActionResult FailureResult(ActionError error, List? clientActions = null) { return new UserActionResult { Success = false, - FrontendActions = frontendActions, + ClientActions = clientActions, Error = error }; } diff --git a/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs index 1a28fd745..e51df365b 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs @@ -1,6 +1,8 @@ using System.Net; using System.Net.Http.Headers; using System.Text; +using System.Text.Json; +using Altinn.App.Api.Models; using Altinn.App.Api.Tests.Data; using Altinn.App.Api.Tests.Utils; using Altinn.App.Core.Features; @@ -12,12 +14,12 @@ namespace Altinn.App.Api.Tests.Controllers; -public class ActionsControllerTests: ApiTestBase, IClassFixture> +public class ActionsControllerTests : ApiTestBase, IClassFixture> { public ActionsControllerTests(WebApplicationFactory factory) : base(factory) { } - + [Fact] public async Task Perform_returns_403_if_user_not_authorized() { @@ -29,13 +31,14 @@ public async Task Perform_returns_403_if_user_not_authorized() TestData.PrepareInstance(org, app, 1337, guid); string token = PrincipalUtil.GetToken(1000, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); // Cleanup testdata TestData.DeleteInstanceAndData(org, app, 1337, guid); - + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); } - + [Fact] public async Task Perform_returns_401_if_user_not_authenticated() { @@ -45,13 +48,14 @@ public async Task Perform_returns_401_if_user_not_authenticated() Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.DeleteInstance(org, app, 1337, guid); TestData.PrepareInstance(org, app, 1337, guid); - HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); // Cleanup testdata TestData.DeleteInstanceAndData(org, app, 1337, guid); - + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } - + [Fact] public async Task Perform_returns_401_if_userId_is_null() { @@ -63,13 +67,14 @@ public async Task Perform_returns_401_if_userId_is_null() TestData.PrepareInstance(org, app, 1337, guid); string token = PrincipalUtil.GetToken(null, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, "application/json")); // Cleanup testdata TestData.DeleteInstanceAndData(org, app, 1337, guid); - + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } - + [Fact] public async Task Perform_returns_400_if_action_is_null() { @@ -81,13 +86,14 @@ public async Task Perform_returns_400_if_action_is_null() TestData.PrepareInstance(org, app, 1337, guid); string token = PrincipalUtil.GetToken(1000, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":null}", Encoding.UTF8, "application/json")); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":null}", Encoding.UTF8, "application/json")); // Cleanup testdata TestData.DeleteInstanceAndData(org, app, 1337, guid); - + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } - + [Fact] public async Task Perform_returns_409_if_process_not_started() { @@ -99,13 +105,14 @@ public async Task Perform_returns_409_if_process_not_started() TestData.PrepareInstance(org, app, 1337, guid); string token = PrincipalUtil.GetToken(1000, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); // Cleanup testdata TestData.DeleteInstanceAndData(org, app, 1337, guid); - + response.StatusCode.Should().Be(HttpStatusCode.Conflict); } - + [Fact] public async Task Perform_returns_409_if_process_ended() { @@ -117,20 +124,18 @@ public async Task Perform_returns_409_if_process_ended() TestData.PrepareInstance(org, app, 1337, guid); string token = PrincipalUtil.GetToken(1000, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); // Cleanup testdata TestData.DeleteInstanceAndData(org, app, 1337, guid); - + response.StatusCode.Should().Be(HttpStatusCode.Conflict); } - + [Fact] public async Task Perform_returns_200_if_action_succeeded() { - OverrideServicesForThisTest = (services) => - { - services.AddTransient(); - }; + OverrideServicesForThisTest = (services) => { services.AddTransient(); }; var org = "tdd"; var app = "task-action"; HttpClient client = GetRootedClient(org, app); @@ -139,20 +144,32 @@ public async Task Perform_returns_200_if_action_succeeded() TestData.PrepareInstance(org, app, 1337, guid); string token = PrincipalUtil.GetToken(1000, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); // Cleanup testdata TestData.DeleteInstanceAndData(org, app, 1337, guid); - + response.StatusCode.Should().Be(HttpStatusCode.OK); + var content = await response.Content.ReadAsStringAsync(); + var expectedString = """ + { + "updatedDataModels": null, + "clientActions": [ + { + "name": "nextPage", + "metadata": null + } + ], + "error": null + } + """; + CompareResult(expectedString, content); } - + [Fact] public async Task Perform_returns_400_if_action_failed() { - OverrideServicesForThisTest = (services) => - { - services.AddTransient(); - }; + OverrideServicesForThisTest = (services) => { services.AddTransient(); }; var org = "tdd"; var app = "task-action"; HttpClient client = GetRootedClient(org, app); @@ -161,20 +178,18 @@ public async Task Perform_returns_400_if_action_failed() TestData.PrepareInstance(org, app, 1337, guid); string token = PrincipalUtil.GetToken(1001, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json")); // Cleanup testdata TestData.DeleteInstanceAndData(org, app, 1337, guid); - + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } - + [Fact] public async Task Perform_returns_404_if_action_implementation_not_found() { - OverrideServicesForThisTest = (services) => - { - services.AddTransient(); - }; + OverrideServicesForThisTest = (services) => { services.AddTransient(); }; var org = "tdd"; var app = "task-action"; HttpClient client = GetRootedClient(org, app); @@ -183,23 +198,34 @@ public async Task Perform_returns_404_if_action_implementation_not_found() TestData.PrepareInstance(org, app, 1337, guid); string token = PrincipalUtil.GetToken(1001, null, 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", new StringContent("{\"action\":\"notfound\"}", Encoding.UTF8, "application/json")); + using HttpResponseMessage response = await client.PostAsync($"/{org}/{app}/instances/1337/{guid}/actions", + new StringContent("{\"action\":\"notfound\"}", Encoding.UTF8, "application/json")); // Cleanup testdata TestData.DeleteInstanceAndData(org, app, 1337, guid); - + response.StatusCode.Should().Be(HttpStatusCode.NotFound); } + + + //TODO: replace this assertion with a proper one once fluentassertions has a json compare feature scheduled for v7 https://github.com/fluentassertions/fluentassertions/issues/2205 + private static void CompareResult(string expectedString, string actualString) + { + T? expected = JsonSerializer.Deserialize(expectedString); + T? actual = JsonSerializer.Deserialize(actualString); + actual.Should().BeEquivalentTo(expected); + } } public class LookupAction : IUserAction { public string Id => "lookup"; + public async Task HandleAction(UserActionContext context) { await Task.CompletedTask; if (context.UserId == 1000) { - return UserActionResult.SuccessResult(); + return UserActionResult.SuccessResult(new List() { ClientAction.NextPage() }); } return UserActionResult.FailureResult(new ActionError()); diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/process/process.bpmn b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/process/process.bpmn index 4a02f91aa..97306f98c 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/process/process.bpmn +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/task-action/config/process/process.bpmn @@ -12,7 +12,7 @@ data complete - lookup + lookup diff --git a/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs index 206a66835..8859288d4 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/UserActionServiceTests.cs @@ -69,7 +69,7 @@ internal class DummyUserAction2 : IUserAction public Task HandleAction(UserActionContext context) { - return Task.FromResult(UserActionResult.SuccessResult()); + return Task.FromResult(UserActionResult.SuccessResult(new List() { ClientAction.NextPage() })); } } } \ 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 32f4b08cb..26f21b441 100644 --- a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs @@ -283,7 +283,7 @@ private async Task AuthorizeActions_returns_list_of_UserActions_with_auth_decisi new AltinnAction("read"), new AltinnAction("write"), new AltinnAction("brew-coffee"), - new AltinnAction("drink-coffee", ActionType.UserAction), + new AltinnAction("drink-coffee", ActionType.ServerAction), }; var actionsStrings = new List() { "read", "write", "brew-coffee", "drink-coffee" }; @@ -325,7 +325,7 @@ private async Task AuthorizeActions_returns_list_of_UserActions_with_auth_decisi new UserAction() { Id = "drink-coffee", - ActionType = ActionType.UserAction, + ActionType = ActionType.ServerAction, Authorized = false } }; diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs index 5b7803ab1..e13cb4504 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessReaderTests.cs @@ -314,7 +314,7 @@ public void GetFlowElement_returns_ProcessTask_with_id() AltinnActions = new List() { new("submit", ActionType.ProcessAction), - new("lookup", ActionType.UserAction) + new("lookup", ActionType.ServerAction) }, TaskType = "data", SignatureConfiguration = new() diff --git a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn index 124c6ba6e..47603d6a3 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn +++ b/test/Altinn.App.Core.Tests/Internal/Process/TestData/simple-gateway-default.bpmn @@ -18,7 +18,7 @@ submit - lookup + lookup data From db957c0faa9c8185b56cb4eba86e559331b77b7b Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Wed, 13 Dec 2023 12:57:58 +0100 Subject: [PATCH 39/46] Multipart form data (#329) * Let ModelDeserializer return a Result type instead of nullable * Remove NullDataProcessor and let user registrer multiple processors * Update IDataProcessor interface and support multipart data write * Update v7tov8 script for new IDataProcessor interface Also ignore CS1998 in apps (warning when not awaiting anything in async hooks) * Code review updates * Update src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs Co-authored-by: Vemund Gaukstad * First draft of tests for controllers * Finish writing tests * Set partyId in token to null * Fix bad case for roles folder in testsetup * Add partyId correctly in test tokens Also add a Dockerfile for running tests on linux * Use proper parsing library to decode multipart/form-data requests * Continue returning changed only changed values from data put. * Fix so that it compiles (still failing tests) * Fix tests by disabeling redirects in applciationfactory client For some reason backend returns 303 See Other when datamodel updates are availible. This is a redirect code and crashed the tests as no Location header was set. * Add more tests * More tests * Update src/Altinn.App.Api/Controllers/DataController.cs Co-authored-by: Vemund Gaukstad * Fix code smells * Fix tests * Tests OK now? * More tests for model deserializer --------- Co-authored-by: Vemund Gaukstad --- cli-tools/altinn-app-cli/Program.cs | 10 +- .../CodeRewriters/IDataProcessorRewriter.cs | 179 +++++++++++++++ .../ProjectRewriters/ProjectFileRewriter.cs | 36 ++- .../Controllers/DataController.cs | 43 ++-- .../Controllers/InstancesController.cs | 8 +- .../Controllers/StatelessDataController.cs | 71 ++++-- .../Extensions/ServiceCollectionExtensions.cs | 1 - .../DataProcessing/NullDataProcessor.cs | 22 -- .../Features/IDataProcessor.cs | 11 +- src/Altinn.App.Core/Helpers/JsonHelper.cs | 42 ++-- .../Serialization/ModelDeserializer.cs | 101 ++++---- .../Serialization/ModelDeserializerResult.cs | 64 ++++++ .../Clients/Storage/DataClient.cs | 18 +- .../Controllers/DataController_PutTests.cs | 217 ++++++++++++++++++ .../InstancesController_PostNewInstance.cs | 119 ++++++++++ .../StatelessDataControllerTests.cs | 12 +- .../CustomWebApplicationFactory.cs | 2 +- .../config/applicationmetadata.json | 2 +- .../contributer-restriction/models/Skjema.cs | 87 +++++++ .../Helpers/JsonHelperTests.cs | 9 +- .../Helpers/ModelDeserializerTests.cs | 144 ++++++++++++ .../Implementation/NullDataProcessorTests.cs | 54 ----- 22 files changed, 1028 insertions(+), 224 deletions(-) create mode 100644 cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/IDataProcessorRewriter.cs delete mode 100644 src/Altinn.App.Core/Features/DataProcessing/NullDataProcessor.cs create mode 100644 src/Altinn.App.Core/Helpers/Serialization/ModelDeserializerResult.cs create mode 100644 test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs create mode 100644 test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs create mode 100644 test/Altinn.App.Core.Tests/Helpers/ModelDeserializerTests.cs delete mode 100644 test/Altinn.App.Core.Tests/Implementation/NullDataProcessorTests.cs diff --git a/cli-tools/altinn-app-cli/Program.cs b/cli-tools/altinn-app-cli/Program.cs index 08b341c55..a60e612a1 100644 --- a/cli-tools/altinn-app-cli/Program.cs +++ b/cli-tools/altinn-app-cli/Program.cs @@ -1,4 +1,4 @@ -using System.CommandLine; +using System.CommandLine; using System.CommandLine.Invocation; using System.Reflection; using altinn_app_cli.v7Tov8.AppSettingsRewriter; @@ -206,12 +206,20 @@ static async Task UpgradeCode(string projectFile) { await File.WriteAllTextAsync(sourceTree.FilePath, newSource.ToFullString()); } + UsingRewriter usingRewriter = new(); var newUsingSource = usingRewriter.Visit(newSource); if (newUsingSource != newSource) { await File.WriteAllTextAsync(sourceTree.FilePath, newUsingSource.ToFullString()); } + + DataProcessorRewriter dataProcessorRewriter = new(sm); + var dataProcessorSource = dataProcessorRewriter.Visit(newUsingSource); + if (dataProcessorSource != newUsingSource) + { + await File.WriteAllTextAsync(sourceTree.FilePath, dataProcessorSource.ToFullString()); + } } Console.WriteLine("References and using upgraded"); diff --git a/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/IDataProcessorRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/IDataProcessorRewriter.cs new file mode 100644 index 000000000..b73916f00 --- /dev/null +++ b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/IDataProcessorRewriter.cs @@ -0,0 +1,179 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace altinn_app_cli.v7Tov8.CodeRewriters +{ + public class DataProcessorRewriter : CSharpSyntaxRewriter + { + private readonly SemanticModel semanticModel; + + public DataProcessorRewriter(SemanticModel semanticModel) + { + this.semanticModel = semanticModel; + } + + public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) + { + // Ignore any classes that don't implement `IDataProcessor` (consider using semantic model to ensure correct reference) + if (node.BaseList?.Types.Any(t => t.Type.ToString() == "IDataProcessor") == true) + { + var processDataWrite = node.Members.OfType() + .FirstOrDefault(m => m.Identifier.ValueText == "ProcessDataWrite"); + if (processDataWrite is not null) + { + node = node.ReplaceNode(processDataWrite, Update_DataProcessWrite(processDataWrite)); + } + + var processDataRead = node.Members.OfType() + .FirstOrDefault(m => m.Identifier.ValueText == "ProcessDataRead"); + if (processDataRead is not null) + { + node = node.ReplaceNode(processDataRead, Update_DataProcessRead(processDataRead)); + } + } + + return base.VisitClassDeclaration(node); + } + + private MethodDeclarationSyntax Update_DataProcessRead(MethodDeclarationSyntax processDataRead) + { + if (processDataRead.ParameterList.Parameters.Count == 3 && + processDataRead.ReturnType.ToString() == "Task") + { + processDataRead = ChangeReturnType_FromTaskBool_ToTask(processDataRead); + } + + return processDataRead; + } + + private MethodDeclarationSyntax Update_DataProcessWrite(MethodDeclarationSyntax processDataWrite) + { + if (processDataWrite.ParameterList.Parameters.Count == 3 && + processDataWrite.ReturnType.ToString() == "Task") + { + processDataWrite = AddParameter_ChangedFields(processDataWrite); + processDataWrite = ChangeReturnType_FromTaskBool_ToTask(processDataWrite); + } + + return processDataWrite; + } + + private MethodDeclarationSyntax AddParameter_ChangedFields(MethodDeclarationSyntax method) + { + return method.ReplaceNode(method.ParameterList, + method.ParameterList.AddParameters(SyntaxFactory.Parameter(SyntaxFactory.Identifier("changedFields")) + .WithLeadingTrivia(SyntaxFactory.Space) + .WithType(SyntaxFactory.ParseTypeName("System.Collections.Generic.Dictionary?")) + .WithLeadingTrivia(SyntaxFactory.Space))); + } + + private MethodDeclarationSyntax ChangeReturnType_FromTaskBool_ToTask(MethodDeclarationSyntax method) + { + if (method.ReturnType.ToString() == "Task") + { + var returnTypeRewriter = new ReturnTypeTaskBooleanRewriter(); + method = (MethodDeclarationSyntax)returnTypeRewriter.Visit(method)!; + } + + return method; + + } + } + + public class ReturnTypeTaskBooleanRewriter : CSharpSyntaxRewriter + { + public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) + { + if (node.ReturnType.ToString() == "Task") + { + // Change return type + node = node.WithReturnType( + SyntaxFactory.ParseTypeName("Task").WithTrailingTrivia(SyntaxFactory.Space)); + } + return base.VisitMethodDeclaration(node); + } + + public override SyntaxNode? VisitBlock(BlockSyntax node) + { + foreach (var returnStatementSyntax in node.Statements.OfType()) + { + var leadingTrivia = returnStatementSyntax.GetLeadingTrivia(); + var trailingTrivia = returnStatementSyntax.GetTrailingTrivia(); + // When we add multiple lines of code, we need the indentation and a newline + var leadingTriviaMiddle = leadingTrivia.LastOrDefault(t => t.IsKind(SyntaxKind.WhitespaceTrivia)); + var trailingTriviaMiddle = trailingTrivia.FirstOrDefault(t => t.IsKind(SyntaxKind.EndOfLineTrivia)); + // If we don't find a newline, just guess that LF is used. Will likely work anyway. + if (trailingTriviaMiddle == default) trailingTriviaMiddle = SyntaxFactory.LineFeed; + + + switch (returnStatementSyntax.Expression) + { + // return true/false/variableName + case IdentifierNameSyntax: + case LiteralExpressionSyntax: + case null: + node = node.ReplaceNode(returnStatementSyntax, + SyntaxFactory.ReturnStatement() + .WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia)); + break; + // case "Task.FromResult(...)": + case InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Expression: IdentifierNameSyntax { Identifier: {Text: "Task" } }, + Name: { Identifier: {Text: "FromResult"}} + }, + ArgumentList: { Arguments: { Count: 1 } } + }: + node = node.ReplaceNode(returnStatementSyntax, + SyntaxFactory.ReturnStatement(SyntaxFactory.ParseExpression(" Task.CompletedTask")) + .WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia)); + break; + // case "await Task.FromResult(...)": + // Assume we need an await to silence CS1998 and rewrite to + // await Task.CompletedTask; return; + // Could be dropped if we ignore CS1998 + case AwaitExpressionSyntax + { + Expression: InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Expression: IdentifierNameSyntax { Identifier: {Text: "Task" } }, + Name: { Identifier: {Text: "FromResult"}} + }, + ArgumentList: { Arguments: [{Expression: IdentifierNameSyntax or LiteralExpressionSyntax}]} + } + }: + node = node.WithStatements(node.Statements.ReplaceRange(returnStatementSyntax, new StatementSyntax[] + { + // Uncomment if cs1998 isn't disabled + // SyntaxFactory.ParseStatement("await Task.CompletedTask;") + // .WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTriviaMiddle), + + SyntaxFactory.ReturnStatement() + .WithLeadingTrivia(leadingTriviaMiddle).WithTrailingTrivia(trailingTrivia), + + })); + break; + // Just add move the return; statement after the existing return value + default: + node = node.WithStatements(node.Statements.ReplaceRange(returnStatementSyntax, + new StatementSyntax[] + { + SyntaxFactory.ExpressionStatement(returnStatementSyntax.Expression) + .WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTriviaMiddle), + + SyntaxFactory.ReturnStatement() + .WithLeadingTrivia(leadingTriviaMiddle).WithTrailingTrivia(trailingTrivia), + })); + break; + } + } + + return base.VisitBlock(node); + } + } +} \ No newline at end of file diff --git a/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs index 614d7a491..a34160194 100644 --- a/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs +++ b/cli-tools/altinn-app-cli/v7Tov8/ProjectRewriters/ProjectFileRewriter.cs @@ -23,17 +23,41 @@ public ProjectFileRewriter(string projectFilePath, string targetVersion = "8.0.0 public async Task Upgrade() { var altinnAppCoreElements = GetAltinnAppCoreElement(); + altinnAppCoreElements?.ForEach(c => c.Attribute("Version")?.SetValue(targetVersion)); + var altinnAppApiElements = GetAltinnAppApiElement(); - if (altinnAppCoreElements != null && altinnAppApiElements != null) - { - altinnAppCoreElements.ForEach(c => c.Attribute("Version")?.SetValue(targetVersion)); - altinnAppApiElements.ForEach(a => a.Attribute("Version")?.SetValue(targetVersion)); - } - + altinnAppApiElements?.ForEach(a => a.Attribute("Version")?.SetValue(targetVersion)); + + IgnoreWarnings("1591", "1998"); // Require xml doc and await in async methods + GetTargetFrameworkElement()?.ForEach(t => t.SetValue(targetFramework)); + await Save(); } + private void IgnoreWarnings(params string[] warnings) + { + var noWarn = doc.Root?.Elements("PropertyGroup").Elements("NoWarn").ToList(); + switch (noWarn?.Count) + { + case 0: + doc.Root?.Elements("PropertyGroup").First().Add(new XElement("NoWarn", "$(NoWarn);" + string.Join(';', warnings))); + break; + + case 1: + var valueElement = noWarn.First(); + foreach (var warning in warnings) + { + if (!valueElement.Value.Contains(warning)) + { + valueElement.SetValue($"{valueElement.Value};{warning}"); + } + } + + break; + } + } + private List? GetAltinnAppCoreElement() { return doc.Root?.Elements("ItemGroup").Elements("PackageReference").Where(x => x.Attribute("Include")?.Value == "Altinn.App.Core").ToList(); diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index 1f168beb5..eac7f46be 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -39,7 +39,7 @@ public class DataController : ControllerBase { private readonly ILogger _logger; private readonly IDataClient _dataClient; - private readonly IDataProcessor _dataProcessor; + private readonly IEnumerable _dataProcessors; private readonly IInstanceClient _instanceClient; private readonly IInstantiationProcessor _instantiationProcessor; private readonly IAppModel _appModel; @@ -58,7 +58,7 @@ public class DataController : ControllerBase /// instance service to store instances /// Instantiation processor /// A service with access to data storage. - /// Serive implemnting logic during data read/write + /// Services implementing logic during data read/write /// Service for generating app model /// The apps resource service /// The app metadata service @@ -71,7 +71,7 @@ public DataController( IInstanceClient instanceClient, IInstantiationProcessor instantiationProcessor, IDataClient dataClient, - IDataProcessor dataProcessor, + IEnumerable dataProcessors, IAppModel appModel, IAppResources appResourcesService, IPrefill prefillService, @@ -85,7 +85,7 @@ public DataController( _instanceClient = instanceClient; _instantiationProcessor = instantiationProcessor; _dataClient = dataClient; - _dataProcessor = dataProcessor; + _dataProcessors = dataProcessors; _appModel = appModel; _appResourcesService = appResourcesService; _appMetadata = appMetadata; @@ -175,7 +175,7 @@ public async Task Create( _logger.LogError(errorMessage); return BadRequest(await GetErrorDetails(new List { error })); } - + bool parseSuccess = Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues); string? filename = parseSuccess ? DataRestrictionValidation.GetFileNameFromHeader(headerValues) : null; @@ -466,12 +466,14 @@ private async Task CreateAppModelData( else { ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) + if (deserializerResult.HasError) { - return BadRequest(deserializer.Error); + return BadRequest(deserializerResult.Error); } + + appModel = deserializerResult.Model; } // runs prefill from repo configuration if config exists @@ -582,7 +584,11 @@ private async Task GetFormData( return BadRequest($"Did not find form data for data element {dataGuid}"); } - await _dataProcessor.ProcessDataRead(instance, dataGuid, appModel); + foreach (var dataProcessor in _dataProcessors) + { + _logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", appModel.GetType().Name, dataProcessor.GetType().Name); + await dataProcessor.ProcessDataRead(instance, dataGuid, appModel); + } string? userOrgClaim = User.GetOrg(); if (userOrgClaim == null || !org.Equals(userOrgClaim, StringComparison.InvariantCultureIgnoreCase)) @@ -616,26 +622,21 @@ private async Task PutFormData(string org, string app, Instance in Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - object? serviceModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - - if (!string.IsNullOrEmpty(deserializer.Error)) - { - return BadRequest(deserializer.Error); - } + ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (serviceModel == null) + if (deserializerResult.HasError) { - return BadRequest("No data found in content"); + return BadRequest(deserializerResult.Error); } - Dictionary? changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, serviceModel, _dataProcessor, _logger); + Dictionary? changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, deserializerResult.Model, _dataProcessors, deserializerResult.ReportedChanges, _logger); - await UpdatePresentationTextsOnInstance(instance, dataType, serviceModel); - await UpdateDataValuesOnInstance(instance, dataType, serviceModel); + await UpdatePresentationTextsOnInstance(instance, dataType, deserializerResult.Model); + await UpdateDataValuesOnInstance(instance, dataType, deserializerResult.Model); // Save Formdata to database DataElement updatedDataElement = await _dataClient.UpdateData( - serviceModel, + deserializerResult.Model, instanceGuid, _appModel.GetModelType(classRef), org, diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index c6fbe0f87..cac36b92f 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -971,13 +971,15 @@ private async Task StorePrefillParts(Instance instance, ApplicationMetadata appI } ModelDeserializer deserializer = new ModelDeserializer(_logger, type); - object? data = await deserializer.DeserializeAsync(part.Stream, part.ContentType); + ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(part.Stream, part.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error) || data is null) + if (deserializerResult.HasError) { - throw new InvalidOperationException(deserializer.Error); + throw new InvalidOperationException(deserializerResult.Error); } + object data = deserializerResult.Model; + await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, part.Name!, data); await _instantiationProcessor.DataCreation(instance, data, null); diff --git a/src/Altinn.App.Api/Controllers/StatelessDataController.cs b/src/Altinn.App.Api/Controllers/StatelessDataController.cs index a222724d1..9a696e031 100644 --- a/src/Altinn.App.Api/Controllers/StatelessDataController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessDataController.cs @@ -33,7 +33,7 @@ public class StatelessDataController : ControllerBase private readonly ILogger _logger; private readonly IAppModel _appModel; private readonly IAppResources _appResourcesService; - private readonly IDataProcessor _dataProcessor; + private readonly IEnumerable _dataProcessors; private readonly IPrefill _prefillService; private readonly IAltinnPartyClient _altinnPartyClientClient; private readonly IPDP _pdp; @@ -45,21 +45,21 @@ public class StatelessDataController : ControllerBase private const string OrgPrefix = "org"; /// - /// The stateless data controller is responsible for creating and updating stateles data elements. + /// The stateless data controller is responsible for creating and updating stateless data elements. /// public StatelessDataController( ILogger logger, IAppModel appModel, IAppResources appResourcesService, - IDataProcessor dataProcessor, IPrefill prefillService, IAltinnPartyClient altinnPartyClientClient, - IPDP pdp) + IPDP pdp, + IEnumerable dataProcessors) { _logger = logger; _appModel = appModel; _appResourcesService = appResourcesService; - _dataProcessor = dataProcessor; + _dataProcessors = dataProcessors; _prefillService = prefillService; _altinnPartyClientClient = altinnPartyClientClient; _pdp = pdp; @@ -115,12 +115,24 @@ public async Task Get( // runs prefill from repo configuration if config exists await _prefillService.PrefillDataModel(owner.PartyId, dataType, appModel); - Instance virutalInstance = new Instance() { InstanceOwner = owner }; - await _dataProcessor.ProcessDataRead(virutalInstance, null, appModel); + Instance virtualInstance = new Instance() { InstanceOwner = owner }; + await ProcessAllDataWrite(virtualInstance, appModel); return Ok(appModel); } + private async Task ProcessAllDataWrite(Instance virtualInstance, object appModel) + { + foreach (var dataProcessor in _dataProcessors) + { + _logger.LogInformation( + "ProcessDataRead for {modelType} using {dataProcesor}", + appModel.GetType().Name, + dataProcessor.GetType().Name); + await dataProcessor.ProcessDataRead(virtualInstance, null, appModel); + } + } + /// /// Create a new data object of the defined data type /// @@ -148,10 +160,8 @@ public async Task GetAnonymous([FromQuery] string dataType) } object appModel = _appModel.Create(classRef); - - var virutalInstance = new Instance(); - - await _dataProcessor.ProcessDataRead(virutalInstance, null, appModel); + var virtualInstance = new Instance(); + await ProcessAllDataWrite(virtualInstance, appModel); return Ok(appModel); } @@ -203,18 +213,24 @@ public async Task Post( } ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) + if (deserializerResult.HasError) { - return BadRequest(deserializer.Error); + return BadRequest(deserializerResult.Error); } + object appModel = deserializerResult.Model; + // runs prefill from repo configuration if config exists await _prefillService.PrefillDataModel(owner.PartyId, dataType, appModel); - Instance virutalInstance = new Instance() { InstanceOwner = owner }; - await _dataProcessor.ProcessDataRead(virutalInstance, null, appModel); + Instance virtualInstance = new Instance() { InstanceOwner = owner }; + foreach (var dataProcessor in _dataProcessors) + { + _logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", appModel.GetType().Name, dataProcessor.GetType().Name); + await dataProcessor.ProcessDataRead(virtualInstance, null, appModel); + } return Ok(appModel); } @@ -246,25 +262,30 @@ public async Task PostAnonymous([FromQuery] string dataType) } ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) + if (deserializerResult.HasError) { - return BadRequest(deserializer.Error); + return BadRequest(deserializerResult.Error); } - Instance virutalInstance = new Instance(); - await _dataProcessor.ProcessDataRead(virutalInstance, null, appModel); + Instance virtualInstance = new Instance(); + var appModel = deserializerResult.Model; + foreach (var dataProcessor in _dataProcessors) + { + _logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", appModel.GetType().Name, dataProcessor.GetType().Name); + await dataProcessor.ProcessDataRead(virtualInstance, null, appModel); + } - return Ok(appModel); + return Ok(deserializerResult.Model); } - private async Task GetInstanceOwner(string partyFromHeader) + private async Task GetInstanceOwner(string? partyFromHeader) { // Use the party id of the logged in user, if no party id is given in the header - // Not sure if this is really used anywhere. It doesn't seem usefull, as you'd + // Not sure if this is really used anywhere. It doesn't seem useful, as you'd // always want to create an instance based on the selected party, not the person - // you happend to log in as. + // you happened to log in as. if (partyFromHeader is null) { var partyId = Request.HttpContext.User.GetPartyIdAsInt(); diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 10c77e740..7b91c4794 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -147,7 +147,6 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); - services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); diff --git a/src/Altinn.App.Core/Features/DataProcessing/NullDataProcessor.cs b/src/Altinn.App.Core/Features/DataProcessing/NullDataProcessor.cs deleted file mode 100644 index aa2b0c6df..000000000 --- a/src/Altinn.App.Core/Features/DataProcessing/NullDataProcessor.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Features.DataProcessing; - -/// -/// Default implementation of the IDataProcessor interface. -/// This implementation does not do any thing to the data -/// -public class NullDataProcessor: IDataProcessor -{ - /// - public async Task ProcessDataRead(Instance instance, Guid? dataId, object data) - { - return await Task.FromResult(false); - } - - /// - public async Task ProcessDataWrite(Instance instance, Guid? dataId, object data) - { - return await Task.FromResult(false); - } -} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/IDataProcessor.cs b/src/Altinn.App.Core/Features/IDataProcessor.cs index fb8fcc9d4..270ad65c3 100644 --- a/src/Altinn.App.Core/Features/IDataProcessor.cs +++ b/src/Altinn.App.Core/Features/IDataProcessor.cs @@ -11,15 +11,16 @@ public interface IDataProcessor /// Is called to run custom calculation events defined by app developer when data is read from app /// /// Instance that data belongs to - /// Data id for the data + /// Data id for the data (nullable if stateless) /// The data to perform calculations on - public Task ProcessDataRead(Instance instance, Guid? dataId, object data); - + public Task ProcessDataRead(Instance instance, Guid? dataId, object data); + /// /// Is called to run custom calculation events defined by app developer when data is written to app /// /// Instance that data belongs to - /// Data id for the data + /// Data id for the data (nullable if stateless) /// The data to perform calculations on - public Task ProcessDataWrite(Instance instance, Guid? dataId, object data); + /// optional dictionary of field keys and previous values (untrusted from frontend) + public Task ProcessDataWrite(Instance instance, Guid? dataId, object data, Dictionary? changedFields); } \ No newline at end of file diff --git a/src/Altinn.App.Core/Helpers/JsonHelper.cs b/src/Altinn.App.Core/Helpers/JsonHelper.cs index 075e9d7b3..ee8bb456f 100644 --- a/src/Altinn.App.Core/Helpers/JsonHelper.cs +++ b/src/Altinn.App.Core/Helpers/JsonHelper.cs @@ -16,33 +16,31 @@ public static class JsonHelper /// /// Run DataProcessWrite returning the dictionary of the changed fields. /// - public static async Task?> ProcessDataWriteWithDiff(Instance instance, Guid dataGuid, object serviceModel, IDataProcessor dataProcessor, ILogger logger) + public static async Task?> ProcessDataWriteWithDiff(Instance instance, Guid dataGuid, object serviceModel, IEnumerable dataProcessors, Dictionary? changedFields, ILogger logger) { - string serviceModelJsonString = System.Text.Json.JsonSerializer.Serialize(serviceModel); - - bool changedByCalculation = await dataProcessor.ProcessDataWrite(instance, dataGuid, serviceModel); - - Dictionary? changedFields = null; - if (changedByCalculation) + if (!dataProcessors.Any()) { - string updatedServiceModelString = System.Text.Json.JsonSerializer.Serialize(serviceModel); - try - { - changedFields = FindChangedFields(serviceModelJsonString, updatedServiceModelString); - } - catch (Exception e) - { - logger.LogError(e, "Unable to determine changed fields"); - } + return null; } - // TODO: Consider not bothering frontend with an empty changes list - // if(changedFields?.Count == 0) - // { - // return null; - // } + string serviceModelJsonString = System.Text.Json.JsonSerializer.Serialize(serviceModel); + foreach (var dataProcessor in dataProcessors) + { + logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", serviceModel.GetType().Name, dataProcessor.GetType().Name); + await dataProcessor.ProcessDataWrite(instance, dataGuid, serviceModel, changedFields); + } - return changedFields; + string updatedServiceModelString = System.Text.Json.JsonSerializer.Serialize(serviceModel); + try + { + var changed = FindChangedFields(serviceModelJsonString, updatedServiceModelString); + return changed.Count == 0 ? null : changed; + } + catch (Exception e) + { + logger.LogError(e, "Unable to determine changed fields"); + return null; + } } /// diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs index 7ee5921ac..ae76b600c 100644 --- a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs +++ b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs @@ -1,10 +1,9 @@ -using System; -using System.IO; + using System.Text; -using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; - +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Net.Http.Headers; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -19,11 +18,6 @@ public class ModelDeserializer private readonly ILogger _logger; private readonly Type _modelType; - /// - /// Gets the error message describing what it was that went wrong if there was an issue during deserialization. - /// - public string? Error { get; private set; } - /// /// Initialize a new instance of with a logger and the Type the deserializer should target. /// @@ -41,14 +35,17 @@ public ModelDeserializer(ILogger logger, Type modelType) /// The data stream to deserialize. /// The content type of the stream. /// An instance of the initialized type if deserializing succeed. - public async Task DeserializeAsync(Stream stream, string? contentType) + public async Task DeserializeAsync(Stream stream, string? contentType) { - Error = null; if (contentType == null) { - Error = $"Unknown content type \"null\". Cannot read the data."; - return null; + return ModelDeserializerResult.FromError($"Unknown content type \"null\". Cannot read the data."); + } + + if (contentType.Contains("multipart/form-data")) + { + return await DeserializeMultipartAsync(stream, contentType); } if (contentType.Contains("application/json")) @@ -61,50 +58,74 @@ public ModelDeserializer(ILogger logger, Type modelType) return await DeserializeXmlAsync(stream); } - Error = $"Unknown content type {contentType}. Cannot read the data."; - return null; + return ModelDeserializerResult.FromError($"Unknown content type {contentType}. Cannot read the data."); } - private async Task DeserializeJsonAsync(Stream stream) + private async Task DeserializeMultipartAsync(Stream stream, string contentType) { - Error = null; + MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(contentType); + string boundary = mediaType.Boundary.Value!.Trim('"'); + var reader = new MultipartReader(boundary, stream); + FormMultipartSection? firstSection = (await reader.ReadNextSectionAsync())?.AsFormDataSection(); + if (firstSection?.Name != "dataModel") + { + return ModelDeserializerResult.FromError("First entry in multipart serialization must have name=\"dataModel\""); + } + var modelResult = await DeserializeJsonAsync(firstSection.Section.Body); + if (modelResult.HasError) + { + return modelResult; + } + + FormMultipartSection? secondSection = (await reader.ReadNextSectionAsync())?.AsFormDataSection(); + Dictionary? reportedChanges = null; + if (secondSection is not null) + { + if (secondSection.Name != "previousValues") + { + return ModelDeserializerResult.FromError("Second entry in multipart serialization must have name=\"previousValues\""); + } + reportedChanges = await System.Text.Json.JsonSerializer.DeserializeAsync>(secondSection.Section.Body); + if (await reader.ReadNextSectionAsync() != null) + { + return ModelDeserializerResult.FromError("Multipart request had more than 2 elements. Only \"dataModel\" and the optional \"previousValues\" are supported."); + } + } + return ModelDeserializerResult.FromSuccess(modelResult.Model, reportedChanges); + } + private async Task DeserializeJsonAsync(Stream stream) + { try { using StreamReader reader = new StreamReader(stream, Encoding.UTF8); string content = await reader.ReadToEndAsync(); - return JsonConvert.DeserializeObject(content, _modelType)!; + return ModelDeserializerResult.FromSuccess(JsonConvert.DeserializeObject(content, _modelType)); } catch (JsonReaderException jsonReaderException) { - Error = jsonReaderException.Message; - return null; + return ModelDeserializerResult.FromError(jsonReaderException.Message); } catch (Exception ex) { - string message = $"Unexpected exception when attempting to deserialize JSON into '{_modelType}'"; - _logger.LogError(ex, message); - Error = message; - return null; + _logger.LogError(ex, "Unexpected exception when attempting to deserialize JSON into '{modelType}'", _modelType); + return ModelDeserializerResult.FromError($"Unexpected exception when attempting to deserialize JSON into '{_modelType}'"); } + } - private async Task DeserializeXmlAsync(Stream stream) + private async Task DeserializeXmlAsync(Stream stream) { - Error = null; - - string streamContent = null; + // In this first try block we assume that the namespace is the same in the model + // and in the XML. This includes no namespace in both. + using StreamReader reader = new StreamReader(stream, Encoding.UTF8); + string? streamContent = await reader.ReadToEndAsync(); try { - // In this first try block we assume that the namespace is the same in the model - // and in the XML. This includes no namespace in both. - using StreamReader reader = new StreamReader(stream, Encoding.UTF8); - streamContent = await reader.ReadToEndAsync(); - using XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(streamContent)); XmlSerializer serializer = new XmlSerializer(_modelType); - return serializer.Deserialize(xmlTextReader); + return ModelDeserializerResult.FromSuccess(serializer.Deserialize(xmlTextReader)); } catch (InvalidOperationException) { @@ -122,21 +143,18 @@ public ModelDeserializer(ILogger logger, Type modelType) using XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(streamContent)); XmlSerializer serializer = new XmlSerializer(_modelType, attributeOverrides); - return serializer.Deserialize(xmlTextReader); + return ModelDeserializerResult.FromSuccess(serializer.Deserialize(xmlTextReader)); } catch (InvalidOperationException invalidOperationException) { // One possible fail condition is if the XML has a namespace, but the model does not, or that the namespaces are different. - Error = $"{invalidOperationException.Message} {invalidOperationException?.InnerException.Message}"; - return null; + return ModelDeserializerResult.FromError($"{invalidOperationException.Message} {invalidOperationException.InnerException?.Message}"); } } catch (Exception ex) { - string message = $"Unexpected exception when attempting to deserialize XML into '{_modelType}'"; - _logger.LogError(ex, message); - Error = message; - return null; + _logger.LogError(ex, "Unexpected exception when attempting to deserialize XML into '{modelType}'", _modelType); + return ModelDeserializerResult.FromError($"Unexpected exception when attempting to deserialize XML into '{_modelType}'"); } } @@ -157,3 +175,4 @@ private static string GetRootElementName(Type modelType) } } } + diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializerResult.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializerResult.cs new file mode 100644 index 000000000..1fec3724d --- /dev/null +++ b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializerResult.cs @@ -0,0 +1,64 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Altinn.App.Core.Helpers.Serialization; + +/// +/// Result used by to indicate the result type +/// +public class ModelDeserializerResult +{ + /// + /// Static factory method to make an object that represents a successfull deserialization + /// If the model is null, set default error message instead (eg: json string "null", deserialize valid to null without exception) + /// + public static ModelDeserializerResult FromSuccess(object? model, Dictionary? reportedChanges = null) => new() + { + Error = model is null ? "Model deserialzied to \"null\"" : null, + Model = model, + ReportedChanges = model is null ? null : reportedChanges, + }; + /// + /// Static factory method to make an object that represents a failed deserialization + /// + public static ModelDeserializerResult FromError(string error) => new() + { + Error = error, + }; + + // private constructor to ensure that invariant is preserved through static factory methods + private ModelDeserializerResult() { } + + /// + /// Utility function to check if the result has errors and set + /// + /// + /// + /// if(!result.HasError) + /// { + /// //result.Model is not null here + /// } + /// else + /// { + /// //result.Error is not null here + /// } + /// + /// + [MemberNotNullWhen(true, nameof(Error))] + [MemberNotNullWhen(false, nameof(Model))] + public bool HasError => Error is not null; + /// + /// Potential error message, If this is set, the other values are null + /// + public string? Error { get; set; } + + /// + /// The actual parsed model + /// + public object? Model { get; set; } + + /// + /// Dictionary with fields and their changed parts + /// + public Dictionary? ReportedChanges { get; set; } + +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index 14bf9e4f0..497e226e4 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -14,13 +14,11 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using Newtonsoft.Json; using System.Xml; using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Data; -using Microsoft.IdentityModel.Tokens; namespace Altinn.App.Core.Infrastructure.Clients.Storage { @@ -169,17 +167,17 @@ public async Task GetFormData(Guid instanceGuid, Type type, string org, HttpResponseMessage response = await _client.GetAsync(token, apiUrl); if (response.IsSuccessStatusCode) { - using Stream stream = await response.Content.ReadAsStreamAsync(); + await using Stream stream = await response.Content.ReadAsStreamAsync(); ModelDeserializer deserializer = new ModelDeserializer(_logger, type); - object? model = await deserializer.DeserializeAsync(stream, "application/xml"); + ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(stream, "application/xml"); - if (deserializer.Error != null || model is null) + if (deserializerResult.HasError) { - _logger.LogError($"Cannot deserialize XML form data read from storage: {deserializer.Error}"); - throw new ServiceException(HttpStatusCode.Conflict, $"Cannot deserialize XML form data from storage {deserializer.Error}"); + _logger.LogError("Cannot deserialize XML form data read from storage: {deserializerError}", deserializerResult.Error); + throw new ServiceException(HttpStatusCode.Conflict, $"Cannot deserialize XML form data from storage {deserializerResult.Error}"); } - return model; + return deserializerResult.Model; } throw await PlatformHttpException.CreateAsync(response); @@ -206,7 +204,7 @@ public async Task> GetBinaryDataList(string org, string app return attachmentList; } - _logger.Log(LogLevel.Error, "Unable to fetch attachment list {0}", response.StatusCode); + _logger.Log(LogLevel.Error, "Unable to fetch attachment list {statusCode}", response.StatusCode); throw await PlatformHttpException.CreateAsync(response); } @@ -214,7 +212,7 @@ public async Task> GetBinaryDataList(string org, string app private static void ExtractAttachments(List dataList, List attachmentList) { List? attachments = null; - IEnumerable attachmentTypes = dataList.GroupBy(m => m.DataType).Select(m => m.FirstOrDefault()); + IEnumerable attachmentTypes = dataList.GroupBy(m => m.DataType).Select(m => m.First()); foreach (DataElement attachmentType in attachmentTypes) { diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs new file mode 100644 index 000000000..77906093c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs @@ -0,0 +1,217 @@ +using Altinn.App.Api.Tests.Utils; +using Microsoft.AspNetCore.Mvc.Testing; +using System.Net.Http.Headers; +using System.Net; +using System.Text.Json; +using Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; +using Altinn.App.Core.Features; +using Xunit; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Moq; + +namespace Altinn.App.Api.Tests.Controllers; + +public class DataController_PutTests : ApiTestBase, IClassFixture> +{ + private readonly Mock _dataProcessor = new(); + + private static readonly JsonSerializerOptions JsonSerializerOptions = new () + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + public DataController_PutTests(WebApplicationFactory factory) : base(factory) + { + OverrideServicesForAllTests = (services) => + { + services.AddSingleton(_dataProcessor.Object); + }; + } + + [Fact] + public async Task PutDataElement_TestSinglePartUpdate_ReturnsOk() + { + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 501337; + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetToken(1337, null); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Create instance + var createResponse = + await client.PostAsync($"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", null); + var createResponseContent = await createResponse.Content.ReadAsStringAsync(); + createResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var createResponseParsed = JsonSerializer.Deserialize(createResponseContent, JsonSerializerOptions)!; + var instanceId = createResponseParsed.Id; + + // Create data element (not sure why it isn't created when the instance is created, autoCreate is true) + using var createDataElementContent = + new StringContent("""{"melding":{"name": "Ivar"}}""", System.Text.Encoding.UTF8, "application/json"); + var createDataElementResponse = + await client.PostAsync($"/{org}/{app}/instances/{instanceId}/data?dataType=default", createDataElementContent); + var createDataElementResponseContent = await createDataElementResponse.Content.ReadAsStringAsync(); + createDataElementResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var createDataElementResponseParsed = + JsonSerializer.Deserialize(createDataElementResponseContent, JsonSerializerOptions)!; + var dataGuid = createDataElementResponseParsed.Id; + + // Update data element + using var updateDataElementContent = + new StringContent("""{"melding":{"name": "Ivar Nesje"}}""", System.Text.Encoding.UTF8, "application/json"); + var response = await client.PutAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}", updateDataElementContent); + response.StatusCode.Should().Be(HttpStatusCode.Created); + + // Verify stored data + var readDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); + readDataElementResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var readDataElementResponseContent = await readDataElementResponse.Content.ReadAsStringAsync(); + var readDataElementResponseParsed = + JsonSerializer.Deserialize(readDataElementResponseContent)!; + readDataElementResponseParsed.Melding.Name.Should().Be("Ivar Nesje"); + + _dataProcessor.Verify(p => p.ProcessDataRead(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny()), Times.Exactly(1)); + _dataProcessor.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny(), It.IsAny>()), Times.Exactly(1)); // TODO: Shouldn't this be 2 because of the first write? + _dataProcessor.VerifyNoOtherCalls(); + } + + [Fact] + public async Task PutDataElement_TestMultiPartUpdate_ReturnsOk() + { + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 501337; + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetToken(1337, null); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Create instance + var createResponse = + await client.PostAsync($"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", null); + var createResponseContent = await createResponse.Content.ReadAsStringAsync(); + createResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var createResponseParsed = JsonSerializer.Deserialize(createResponseContent, JsonSerializerOptions)!; + var instanceId = createResponseParsed.Id; + + // Create data element (not sure why it isn't created when the instance is created, autoCreate is true) + using var createDataElementContent = + new StringContent("""{"melding":{"name": "Ivar"}}""", System.Text.Encoding.UTF8, "application/json"); + var createDataElementResponse = + await client.PostAsync($"/{org}/{app}/instances/{instanceId}/data?dataType=default", + createDataElementContent); + var createDataElementResponseContent = await createDataElementResponse.Content.ReadAsStringAsync(); + createDataElementResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var createDataElementResponseParsed = + JsonSerializer.Deserialize(createDataElementResponseContent, JsonSerializerOptions)!; + var dataGuid = createDataElementResponseParsed.Id; + + // Update data element + using var updateDataElementContent = new MultipartFormDataContent(); + updateDataElementContent.Add(new StringContent("""{"melding":{"name": "Ivar Nesje"}}""", System.Text.Encoding.UTF8, + "application/json"), "dataModel"); + updateDataElementContent.Add(new StringContent("""{"melding.name":"Ivar"}""", System.Text.Encoding.UTF8, + "application/json"), "previousValues"); + + var response = await client.PutAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}", updateDataElementContent); + var responseContent = await response.Content.ReadAsStringAsync(); + response.StatusCode.Should().Be(HttpStatusCode.Created); + + // Verify stored data + var readDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); + var readDataElementResponseContent = await readDataElementResponse.Content.ReadAsStringAsync(); + var readDataElementResponseParsed = + JsonSerializer.Deserialize(readDataElementResponseContent)!; + readDataElementResponseParsed.Melding.Name.Should().Be("Ivar Nesje"); + + // Verify that update response equals the following read response + // responseContent.Should().Be(readDataElementResponseContent); + + _dataProcessor.Verify(p=>p.ProcessDataRead(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny()), Times.Exactly(1)); + _dataProcessor.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny(), It.Is>(d => d.ContainsKey("melding.name"))), Times.Exactly(1)); // TODO: Shouldn't this be 2 because of the first write? + _dataProcessor.VerifyNoOtherCalls(); + } + + [Fact] + public async Task PutDataElement_TestMultiPartUpdateWithCustomDataProcessor_ReturnsOk() + { + // Run the previous test with a custom data processor + _dataProcessor.Setup(d => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>())) + .Returns((Instance instance, Guid dataGuid, object data, Dictionary? previousValues) => + { + if (data is Skjema skjema) + { + skjema.Melding.Toggle = true; + } + + return Task.CompletedTask; + }); + + // Run previous test with different setup + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 501337; + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetToken(1337, null); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Create instance + var createResponse = + await client.PostAsync($"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", null); + var createResponseContent = await createResponse.Content.ReadAsStringAsync(); + createResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var createResponseParsed = JsonSerializer.Deserialize(createResponseContent, JsonSerializerOptions)!; + var instanceId = createResponseParsed.Id; + + // Create data element (not sure why it isn't created when the instance is created, autoCreate is true) + using var createDataElementContent = + new StringContent("""{"melding":{"name": "Ivar"}}""", System.Text.Encoding.UTF8, "application/json"); + var createDataElementResponse = + await client.PostAsync($"/{org}/{app}/instances/{instanceId}/data?dataType=default", + createDataElementContent); + var createDataElementResponseContent = await createDataElementResponse.Content.ReadAsStringAsync(); + createDataElementResponse.StatusCode.Should().Be(HttpStatusCode.Created); + var createDataElementResponseParsed = + JsonSerializer.Deserialize(createDataElementResponseContent, JsonSerializerOptions)!; + var dataGuid = createDataElementResponseParsed.Id; + + // Verify stored data + var firstReadDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); + var firstReadDataElementResponseContent = await firstReadDataElementResponse.Content.ReadAsStringAsync(); + var firstReadDataElementResponseParsed = + JsonSerializer.Deserialize(firstReadDataElementResponseContent)!; + firstReadDataElementResponseParsed.Melding.Name.Should().Be("Ivar"); + firstReadDataElementResponseParsed.Melding.Toggle.Should().BeFalse(); + + // Update data element + using var updateDataElementContent = new MultipartFormDataContent(); + updateDataElementContent.Add(new StringContent("""{"melding":{"name": "Ivar Nesje"}}""", System.Text.Encoding.UTF8, + "application/json"), "\"dataModel\""); + updateDataElementContent.Add(new StringContent("""{"melding.name":"Ivar"}""", System.Text.Encoding.UTF8, + "application/json"), "\"previousValues\""); + + var response = await client.PutAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}", updateDataElementContent); + var responseContent = await response.Content.ReadAsStringAsync(); + response.StatusCode.Should().Be(HttpStatusCode.SeeOther); + + // Verify stored data + var readDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); + var readDataElementResponseContent = await readDataElementResponse.Content.ReadAsStringAsync(); + var readDataElementResponseParsed = + JsonSerializer.Deserialize(readDataElementResponseContent)!; + readDataElementResponseParsed.Melding.Name.Should().Be("Ivar Nesje"); + readDataElementResponseParsed.Melding.Toggle.Should().BeTrue(); + + _dataProcessor.Verify(p=>p.ProcessDataRead(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny()), Times.Exactly(2)); + _dataProcessor.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny(), It.Is>(d => d.ContainsKey("melding.name"))), Times.Exactly(1)); // TODO: Shouldn't this be 2 because of the first write? + _dataProcessor.VerifyNoOtherCalls(); + + } + + +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs new file mode 100644 index 000000000..da7c28354 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs @@ -0,0 +1,119 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json; +using Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; +using Altinn.App.Api.Tests.Utils; +using Altinn.App.Core.Features; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Altinn.App.Api.Tests.Controllers; + +public class InstancesController_PostNewInstanceTests : ApiTestBase, IClassFixture> +{ + private readonly Mock _dataProcessor = new(); + + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + public InstancesController_PostNewInstanceTests(WebApplicationFactory factory) : base(factory) + { + OverrideServicesForAllTests = (services) => + { + services.AddSingleton(_dataProcessor.Object); + }; + } + [Fact] + public async Task PostNewInstanceWithContent_EnsureDataIsPresent() + { + // Setup test data + string testName = nameof(PostNewInstanceWithContent_EnsureDataIsPresent); + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 501337; + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetToken(1337, null); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Create instance data + using var content = new MultipartFormDataContent(); + content.Add(new StringContent($$$"""{{{testName}}}""", System.Text.Encoding.UTF8, "application/xml"), "default"); + + // Create instance + var createResponse = + await client.PostAsync($"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", content); + var createResponseContent = await createResponse.Content.ReadAsStringAsync(); + createResponse.StatusCode.Should().Be(HttpStatusCode.Created, createResponseContent); + + var createResponseParsed = JsonSerializer.Deserialize(createResponseContent, JsonSerializerOptions)!; + + // Verify Data id + var instanceId = createResponseParsed.Id; + createResponseParsed.Data.Should().HaveCount(1, "Create instance should create a data element"); + var dataGuid = createResponseParsed.Data.First().Id; + + + // Verify stored data + var readDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); + readDataElementResponse.StatusCode.Should().Be(HttpStatusCode.OK); + var readDataElementResponseContent = await readDataElementResponse.Content.ReadAsStringAsync(); + var readDataElementResponseParsed = + JsonSerializer.Deserialize(readDataElementResponseContent)!; + readDataElementResponseParsed.Melding.Name.Should().Be(testName); + } + + [Fact] + public async Task PostNewInstanceWithInvalidData_EnsureInvalidResponse() + { + // Should probably be BadRequest, but this is what the current implementation returns + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 501337; + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetToken(1337, null); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Create instance data + using var content = new MultipartFormDataContent(); + content.Add(new StringContent("INVALID XML", System.Text.Encoding.UTF8, "application/xml"), "default"); + + // Create instance + var createResponse = + await client.PostAsync($"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", content); + var createResponseContent = await createResponse.Content.ReadAsStringAsync(); + createResponse.StatusCode.Should().Be(HttpStatusCode.InternalServerError, createResponseContent); + createResponseContent.Should().Contain("Instantiation of data elements failed"); + } + + + [Fact] + public async Task PostNewInstanceWithWrongPartname_EnsureBadRequest() + { + // Setup test data + string testName = nameof(PostNewInstanceWithWrongPartname_EnsureBadRequest); + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 501337; + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetToken(1337, null); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Create instance data + using var content = new MultipartFormDataContent(); + content.Add(new StringContent($$$"""{{{testName}}}""", System.Text.Encoding.UTF8, "application/xml"), "wrongName"); + + // Create instance + var createResponse = + await client.PostAsync($"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", content); + var createResponseContent = await createResponse.Content.ReadAsStringAsync(); + createResponse.StatusCode.Should().Be(HttpStatusCode.BadRequest, createResponseContent); + createResponseContent.Should().Contain("Multipart section named, 'wrongName' does not correspond to an element"); + } +} diff --git a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs index 30fc7e575..e22b16245 100644 --- a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs @@ -40,7 +40,7 @@ public async void Get_Returns_BadRequest_when_dataType_is_null() var pdpMock = new Mock(); ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, altinnAppModelMock.Object, appResourcesMock.Object, - dataProcessorMock.Object, prefillMock.Object, registerMock.Object, pdpMock.Object); + prefillMock.Object, registerMock.Object, pdpMock.Object, new IDataProcessor[] { dataProcessorMock.Object }); string dataType = null!; // this is what we're testing @@ -70,7 +70,7 @@ public async void Get_Returns_BadRequest_when_appResource_classRef_is_null() var dataType = "some-value"; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, appModelMock.Object, appResourcesMock.Object, - dataProcessorMock.Object, prefillMock.Object, registerMock.Object, pdpMock.Object); + prefillMock.Object, registerMock.Object, pdpMock.Object, new IDataProcessor[] { dataProcessorMock.Object }); // Act @@ -175,7 +175,7 @@ public async void Get_Returns_BadRequest_when_instance_owner_is_empty_party_head var dataType = "some-value"; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, appModelMock.Object, appResourcesMock.Object, - dataProcessorMock.Object, prefillMock.Object, registerMock.Object, pdpMock.Object); + prefillMock.Object, registerMock.Object, pdpMock.Object, new IDataProcessor[] { dataProcessorMock.Object }); // Act appResourcesMock.Setup(x => x.GetClassRefForLogicDataType(dataType)).Returns(typeof(DummyModel).FullName!); @@ -206,7 +206,7 @@ public async void Get_Returns_BadRequest_when_instance_owner_is_empty_user_in_co var dataType = "some-value"; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, appModelMock.Object, appResourcesMock.Object, - dataProcessorMock.Object, prefillMock.Object, registerMock.Object, pdpMock.Object); + prefillMock.Object, registerMock.Object, pdpMock.Object, new IDataProcessor[] { dataProcessorMock.Object }); statelessDataController.ControllerContext = new ControllerContext(); statelessDataController.ControllerContext.HttpContext = new DefaultHttpContext(); statelessDataController.ControllerContext.HttpContext.User = new ClaimsPrincipal(new List() @@ -246,7 +246,7 @@ public async void Get_Returns_Forbidden_when_returned_descision_is_Deny() var dataType = "some-value"; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, appModelMock.Object, appResourcesMock.Object, - dataProcessorMock.Object, prefillMock.Object, registerMock.Object, pdpMock.Object); + prefillMock.Object, registerMock.Object, pdpMock.Object, new IDataProcessor[] { dataProcessorMock.Object }); statelessDataController.ControllerContext = new ControllerContext(); statelessDataController.ControllerContext.HttpContext = new DefaultHttpContext(); statelessDataController.ControllerContext.HttpContext.User = new ClaimsPrincipal(new List() @@ -304,7 +304,7 @@ public async void Get_Returns_OK_with_appModel() var classRef = typeof(DummyModel).FullName!; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController(logger, appModelMock.Object, appResourcesMock.Object, - dataProcessorMock.Object, prefillMock.Object, registerMock.Object, pdpMock.Object); + prefillMock.Object, registerMock.Object, pdpMock.Object, new IDataProcessor[] { dataProcessorMock.Object }); statelessDataController.ControllerContext = new ControllerContext(); statelessDataController.ControllerContext.HttpContext = new DefaultHttpContext(); statelessDataController.ControllerContext.HttpContext.User = new ClaimsPrincipal(new List() diff --git a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs index 239ec84fd..5ad78fa26 100644 --- a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs +++ b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs @@ -37,7 +37,7 @@ public HttpClient GetRootedClient(string org, string app) builder.ConfigureServices(services => services.Configure(appSettingSection)); builder.ConfigureTestServices(services => OverrideServicesForAllTests(services)); builder.ConfigureTestServices(OverrideServicesForThisTest); - }).CreateClient(); + }).CreateClient(new WebApplicationFactoryClientOptions() { AllowAutoRedirect = false }); return client; } diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json index a43585704..57f86392d 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json @@ -16,7 +16,7 @@ "maxCount": 1, "appLogic": { "autoCreate": true, - "ClassRef": "App.IntegrationTests.Mocks.Apps.tdd.custom_validation.Skjema" + "ClassRef": "Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models.Skjema" }, "taskId": "Task_1" }, diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs new file mode 100644 index 000000000..877479524 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs @@ -0,0 +1,87 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; + +public class Skjema +{ + [XmlElement("melding", Order = 1)] + [JsonProperty("melding")] + [JsonPropertyName("melding")] + public Dummy Melding { get; set; } = default!; +} + +public class Dummy +{ + [XmlElement("name", Order = 1)] + [JsonProperty("name")] + [JsonPropertyName("name")] + public string Name { get; set; } = default!; + + [XmlElement("random", Order = 2)] + [JsonProperty("random")] + [JsonPropertyName("random")] + public string Random { get; set; } = default!; + + [XmlElement("tags", Order = 3)] + [JsonProperty("tags")] + [JsonPropertyName("tags")] + public string Tags { get; set; } = default!; + + [XmlElement("simple_list", Order = 4)] + [JsonProperty("simple_list")] + [JsonPropertyName("simple_list")] + public ValuesList SimpleList { get; set; } = default!; + + [XmlElement("nested_list", Order = 5)] + [JsonProperty("nested_list")] + [JsonPropertyName("nested_list")] + public List NestedList { get; set; } = default!; + + [XmlElement("toggle", Order = 6)] + [JsonProperty("toggle")] + [JsonPropertyName("toggle")] + public bool Toggle { get; set; } = default!; +} + +public class ValuesList +{ + [XmlElement("simple_keyvalues", Order = 1)] + [JsonProperty("simple_keyvalues")] + [JsonPropertyName("simple_keyvalues")] + public List SimpleKeyvalues { get; set; } = default!; +} + +public class SimpleKeyvalues +{ + [XmlElement("key", Order = 1)] + [JsonProperty("key")] + [JsonPropertyName("key")] + public string Key { get; set; } = default!; + + [XmlElement("doubleValue", Order = 2)] + [JsonProperty("doubleValue")] + [JsonPropertyName("doubleValue")] + public decimal DoubleValue { get; set; } = default!; + + [Range(int.MinValue, int.MaxValue)] + [XmlElement("intValue", Order = 3)] + [JsonProperty("intValue")] + [JsonPropertyName("intValue")] + public decimal IntValue { get; set; } = default!; +} + +public class Nested +{ + [XmlElement("key", Order = 1)] + [JsonProperty("key")] + [JsonPropertyName("key")] + public string Key { get; set; } = default!; + + [XmlElement("values", Order = 2)] + [JsonProperty("values")] + [JsonPropertyName("values")] + public List Values { get; set; } = default!; +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs b/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs index a994def27..8e40554ad 100644 --- a/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs @@ -23,12 +23,12 @@ public class JsonHelperTests var logger = new Mock().Object; var guid = Guid.Empty; var dataProcessorMock = new Mock(); - Func> dataProcessWrite = (instance, guid, model) => Task.FromResult(processDataWriteImpl((TModel)model)); + Func?, Task> dataProcessWrite = (instance, guid, model, changes) => Task.FromResult(processDataWriteImpl((TModel)model)); dataProcessorMock - .Setup((d) => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup((d) => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>())) .Returns(dataProcessWrite); - return await JsonHelper.ProcessDataWriteWithDiff(instance, guid, model, dataProcessorMock.Object, logger); + return await JsonHelper.ProcessDataWriteWithDiff(instance, guid, model, new IDataProcessor[] { dataProcessorMock.Object }, new(), logger); } public class TestModel @@ -84,8 +84,7 @@ public async Task InitializingPropertiesLeadsToNoDiff() return true; }); - // Might be null in the future - diff.Should().BeEmpty(); + diff.Should().BeNull(); } [Fact] diff --git a/test/Altinn.App.Core.Tests/Helpers/ModelDeserializerTests.cs b/test/Altinn.App.Core.Tests/Helpers/ModelDeserializerTests.cs new file mode 100644 index 000000000..0d775d727 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/ModelDeserializerTests.cs @@ -0,0 +1,144 @@ +using System.Text; +using System.Text.Json.Serialization; +using System.Xml.Serialization; +using Altinn.App.Core.Helpers.Serialization; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Helpers; + +public class ModelDeserializerTests +{ + private readonly ILogger _logger = new Mock().Object; + + [XmlRoot("melding")] + public class Melding + { + [JsonPropertyName("test")] + [XmlElement("test")] + public string Test { get; set; } + } + + [Fact] + public async Task TestDeserializeJson() + { + // Arrange + string json = @"{""test"":""test""}"; + + // Act + var deserializer = new ModelDeserializer(_logger, typeof(Melding)); + var result = await deserializer.DeserializeAsync(new MemoryStream(Encoding.UTF8.GetBytes(json)), "application/json"); + + // Assert + result.HasError.Should().BeFalse(); + result.Model.Should().BeOfType().Which.Test.Should().Be("test"); + } + + [Fact] + public async Task TestDeserializeXml() + { + // Arrange + string json = "test"; + + // Act + var deserializer = new ModelDeserializer(_logger, typeof(Melding)); + var result = await deserializer.DeserializeAsync(new MemoryStream(Encoding.UTF8.GetBytes(json)), "application/xml"); + + // Assert + result.HasError.Should().BeFalse(result.Error); + result.Model.Should().BeOfType().Which.Test.Should().Be("test"); + } + + [Fact] + public async Task TestDeserializeInvalidXml() + { + // Arrange + string json = "test"; + + // Act + var deserializer = new ModelDeserializer(_logger, typeof(Melding)); + var result = await deserializer.DeserializeAsync(new MemoryStream(Encoding.UTF8.GetBytes(json)), "application/xml"); + + // Assert + result.HasError.Should().BeTrue(); + result.Error.Should().Contain("The 'testFail' start tag on line 1 position 11 does not match the end tag of 'estFail'. Line 1, position 26."); + } + + [Fact] + public async Task TestDeserializeMultipartWithInvalidFirstContent() + { + // Arrange + string json = @"{""test"":""test""}"; + using var requestContent = new MultipartFormDataContent(); + requestContent.Add(new StringContent(json, Encoding.UTF8, "application/json"), "ddddd"); + requestContent.Add(new StringContent("invalid", Encoding.UTF8, "application/xml"), "dddd"); + + // Act + var deserializer = new ModelDeserializer(_logger, typeof(Melding)); + + var result = await deserializer.DeserializeAsync(await requestContent.ReadAsStreamAsync(), requestContent.Headers.ContentType!.ToString()); + + // Assert + result.HasError.Should().BeTrue(); + result.Error.Should().Contain("First entry in multipart serialization must have name=\"dataModel\""); + } + + [Fact] + public async Task TestDeserializeMultipartWithInvalidSecondContent() + { + // Arrange + string json = @"{""test"":""test""}"; + using var requestContent = new MultipartFormDataContent(); + requestContent.Add(new StringContent(json, Encoding.UTF8, "application/json"), "dataModel"); + requestContent.Add(new StringContent("invalid", Encoding.UTF8, "application/xml"), "dddd"); + + // Act + var deserializer = new ModelDeserializer(_logger, typeof(Melding)); + + var result = await deserializer.DeserializeAsync(await requestContent.ReadAsStreamAsync(), requestContent.Headers.ContentType!.ToString()); + + // Assert + result.HasError.Should().BeTrue(); + result.Error.Should().Contain("Second entry in multipart serialization must have name=\"previousValues\""); + } + + [Fact] + public async Task TestDeserializeMultipart() + { + // Arrange + string json = @"{""test"":""test""}"; + using var requestContent = new MultipartFormDataContent(); + requestContent.Add(new StringContent(json, Encoding.UTF8, "application/json"), "default"); + requestContent.Add(new StringContent("invalid", Encoding.UTF8, "application/xml"), "dddd"); + + // Act + var deserializer = new ModelDeserializer(_logger, typeof(Melding)); + + var result = await deserializer.DeserializeAsync(await requestContent.ReadAsStreamAsync(), requestContent.Headers.ContentType!.ToString()); + + // Assert + result.HasError.Should().BeTrue(); + result.Error.Should().Contain("First entry in multipart serialization must have name=\"dataModel\""); + } + + [Fact] + public async Task TestDeserializeMultipart_UnknownContentType() + { + // Arrange + string json = @"{""test"":""test""}"; + using var requestContent = new MultipartFormDataContent(); + requestContent.Add(new StringContent(json, Encoding.UTF8, "application/json"), "default"); + requestContent.Add(new StringContent("invalid", Encoding.UTF8, "application/xml"), "dddd"); + + // Act + var deserializer = new ModelDeserializer(_logger, typeof(Melding)); + + var result = await deserializer.DeserializeAsync(await requestContent.ReadAsStreamAsync(), "Unknown Content Type"); + + // Assert + result.HasError.Should().BeTrue(); + result.Error.Should().Contain("Unknown content type Unknown Content Type. Cannot read the data."); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Implementation/NullDataProcessorTests.cs b/test/Altinn.App.Core.Tests/Implementation/NullDataProcessorTests.cs deleted file mode 100644 index 39e705c84..000000000 --- a/test/Altinn.App.Core.Tests/Implementation/NullDataProcessorTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Altinn.App.Core.Features.DataProcessing; -using Altinn.App.PlatformServices.Tests.Implementation.TestResources; -using Altinn.Platform.Storage.Interface.Models; -using FluentAssertions; -using Xunit; - -namespace Altinn.App.PlatformServices.Tests.Implementation; - -public class NullDataProcessorTests -{ - [Fact] - public async void NullDataProcessor_ProcessDataRead_makes_no_changes_and_returns_false() - { - // Arrange - var dataProcessor = new NullDataProcessor(); - DummyModel expected = new DummyModel() - { - Name = "Test" - }; - object input = new DummyModel() - { - Name = "Test" - }; - - // Act - var result = await dataProcessor.ProcessDataRead(new Instance(), null, input); - - // Assert - result.Should().BeFalse(); - input.Should().BeEquivalentTo(expected); - } - - [Fact] - public async void NullDataProcessor_ProcessDataWrite_makes_no_changes_and_returns_false() - { - // Arrange - var dataProcessor = new NullDataProcessor(); - DummyModel expected = new DummyModel() - { - Name = "Test" - }; - object input = new DummyModel() - { - Name = "Test" - }; - - // Act - var result = await dataProcessor.ProcessDataWrite(new Instance(), null, input); - - // Assert - result.Should().BeFalse(); - input.Should().BeEquivalentTo(expected); - } -} \ No newline at end of file From 85420887dcc8010fb8d40841ae4b3f9cfdb87f97 Mon Sep 17 00:00:00 2001 From: Mikael Solstad Date: Thu, 14 Dec 2023 12:46:02 +0100 Subject: [PATCH 40/46] feat: add PreviousPage and NavigateToPage client actions (#377) --- .../Models/UserAction/ClientAction.cs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Models/UserAction/ClientAction.cs b/src/Altinn.App.Core/Models/UserAction/ClientAction.cs index 48ec5c976..5b1aeeaa5 100644 --- a/src/Altinn.App.Core/Models/UserAction/ClientAction.cs +++ b/src/Altinn.App.Core/Models/UserAction/ClientAction.cs @@ -18,7 +18,7 @@ public class ClientAction /// [JsonPropertyName("metadata")] public Dictionary? Metadata { get; set; } - + /// /// Creates a nextPage client action /// @@ -31,4 +31,32 @@ public static ClientAction NextPage() }; return frontendAction; } + + /// + /// Creates a previousPage client action + /// + /// + public static ClientAction PreviousPage() + { + var frontendAction = new ClientAction() + { + Name = "previousPage" + }; + return frontendAction; + } + + /// + /// Creates a navigateToPage client action + /// + /// The page that should be navigated to + /// + public static ClientAction NavigateToPage(string page) + { + var frontendAction = new ClientAction() + { + Name = "navigateToPage", + Metadata = new Dictionary { { "page", page } } + }; + return frontendAction; + } } From c018446b0076ca4582fb607185063506f9bcf78c Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Fri, 15 Dec 2023 19:07:11 +0100 Subject: [PATCH 41/46] More fixes for nullability (#380) --- src/Altinn.App.Core/Altinn.App.Core.csproj | 1 - src/Altinn.App.Core/Extensions/HttpContextExtensions.cs | 2 +- src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs | 2 +- .../Clients/Profile/ProfileClientCachingDecorator.cs | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index 51bbd3e3c..e31d41506 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -25,7 +25,6 @@ - diff --git a/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs b/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs index 8ba739d8b..938ca08f4 100644 --- a/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs +++ b/src/Altinn.App.Core/Extensions/HttpContextExtensions.cs @@ -15,7 +15,7 @@ public static class HttpContextExtensions public static StreamContent CreateContentStream(this HttpRequest request) { StreamContent content = new StreamContent(request.Body); - content.Headers.ContentType = MediaTypeHeaderValue.Parse(request.ContentType); + content.Headers.ContentType = MediaTypeHeaderValue.Parse(request.ContentType!); if (request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) { diff --git a/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs b/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs index 9cf73be23..4f4d56084 100644 --- a/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs +++ b/src/Altinn.App.Core/Extensions/XmlToLinqExtensions.cs @@ -27,7 +27,7 @@ public static class XmlToLinqExtensions /// public static XElement AddAttribute(this XElement element, string attributeName, object value) { - element.Add(new XAttribute(attributeName, value.ToString())); + element.Add(new XAttribute(attributeName, value.ToString()!)); return element; } diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs index 70dcd07b2..68662b4c2 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Profile/ProfileClientCachingDecorator.cs @@ -32,11 +32,11 @@ public ProfileClientCachingDecorator(IProfileClient decoratedService, IMemoryCac } /// - public async Task GetUserProfile(int userId) + public async Task GetUserProfile(int userId) { string uniqueCacheKey = "User_UserId_" + userId; - if (_memoryCache.TryGetValue(uniqueCacheKey, out UserProfile user)) + if (_memoryCache.TryGetValue(uniqueCacheKey, out UserProfile? user)) { return user; } From 97da9078b5412be119b56ed71f48f97d76ffd60b Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Fri, 29 Dec 2023 21:53:11 +0100 Subject: [PATCH 42/46] Support using connectionString for ApplicationInsights (#379) This wil become a requirement from may 2025, so adding this early ensures that does not become an requirement for updating apps. https://github.com/microsoft/ApplicationInsights-dotnet/issues/2560 --- .../Extensions/ServiceCollectionExtensions.cs | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs index a7417917c..9c1bbddf4 100644 --- a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs @@ -82,13 +82,29 @@ public static void AddAltinnAppServices(this IServiceCollection services, IConfi private static void AddApplicationInsights(IServiceCollection services, IConfiguration config, IWebHostEnvironment env) { - string applicationInsightsKey = env.IsDevelopment() ? - config["ApplicationInsights:InstrumentationKey"] - : Environment.GetEnvironmentVariable("ApplicationInsights__InstrumentationKey"); - - if (!string.IsNullOrEmpty(applicationInsightsKey)) + string? applicationInsightsKey = env.IsDevelopment() + ? config["ApplicationInsights:InstrumentationKey"] + : Environment.GetEnvironmentVariable("ApplicationInsights__InstrumentationKey"); + string? applicationInsightsConnectionString = env.IsDevelopment() ? + config["ApplicationInsights:ConnectionString"] + : Environment.GetEnvironmentVariable("ApplicationInsights__ConnectionString"); + + if (!string.IsNullOrEmpty(applicationInsightsKey) || !string.IsNullOrEmpty(applicationInsightsConnectionString)) { - services.AddApplicationInsightsTelemetry(applicationInsightsKey); + services.AddApplicationInsightsTelemetry((options) => + { + if (string.IsNullOrEmpty(applicationInsightsConnectionString)) + { +#pragma warning disable CS0618 // Type or member is obsolete + // Set instrumentationKey for compatibility if connectionString does not exist. + options.InstrumentationKey = applicationInsightsKey; +#pragma warning restore CS0618 // Type or member is obsolete + } + else + { + options.ConnectionString = applicationInsightsConnectionString; + } + }); services.AddApplicationInsightsTelemetryProcessor(); services.AddApplicationInsightsTelemetryProcessor(); services.AddSingleton(); From 45cfc6df69b2d75684f26db86c4420e7aededc2d Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Tue, 9 Jan 2024 08:24:22 +0100 Subject: [PATCH 43/46] Chore/382 consistent timezone (#389) * Remove usage of DateTime.Now in favor of DateTime.UtcNow Use DateTime.UtcNow Added some tests Remove Console.WriteLines * Add some simple tests for DefaultEFormidlingService * Fix code smells * Implement suggestions from review --- .../RequestHandling/RequestPartValidator.cs | 8 - .../DefaultEFormidlingService.cs | 13 +- .../Features/Validation/ValidationAppSI.cs | 2 +- .../Controllers/ProcessControllerTests.cs | 2 +- ...b-83ed-44df-85a7-2f104c640bff.pretest.json | 2 +- .../Mocks/DataClientMock.cs | 2 +- .../DefaultEFormidlingServiceTests.cs | 220 ++++++++++++++++++ .../Validators/ValidationAppSITests.cs | 116 ++++++++- 8 files changed, 340 insertions(+), 25 deletions(-) create mode 100644 test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs diff --git a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs index 2142a9495..fc1ead428 100644 --- a/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs +++ b/src/Altinn.App.Api/Helpers/RequestHandling/RequestPartValidator.cs @@ -41,15 +41,7 @@ public RequestPartValidator(Application appInfo) } else { - Console.WriteLine($"// {DateTime.Now} // Debug // Part : {part}"); - Console.WriteLine($"// {DateTime.Now} // Debug // Part name: {part.Name}"); - Console.WriteLine($"// {DateTime.Now} // Debug // appinfo : {appInfo}"); - Console.WriteLine($"// {DateTime.Now} // Debug // appinfo.Id : {appInfo.Id}"); - DataType? dataType = appInfo.DataTypes.Find(e => e.Id == part.Name); - - Console.WriteLine($"// {DateTime.Now} // Debug // elementType : {dataType}"); - if (dataType == null) { return $"Multipart section named, '{part.Name}' does not correspond to an element type in application metadata"; diff --git a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs index cbcae69c0..ef4e291b1 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs @@ -2,6 +2,7 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.EFormidling.Interface; using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Events; using Altinn.App.Core.Models; @@ -9,8 +10,6 @@ using Altinn.Common.EFormidlingClient; using Altinn.Common.EFormidlingClient.Models.SBD; using Altinn.Platform.Storage.Interface.Models; -using AltinnCore.Authentication.Utils; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -23,7 +22,7 @@ public class DefaultEFormidlingService : IEFormidlingService { private readonly ILogger _logger; private readonly IAccessTokenGenerator? _tokenGenerator; - private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IUserTokenProvider _userTokenProvider; private readonly AppSettings? _appSettings; private readonly PlatformSettings? _platformSettings; private readonly IEFormidlingClient? _eFormidlingClient; @@ -38,7 +37,7 @@ public class DefaultEFormidlingService : IEFormidlingService /// public DefaultEFormidlingService( ILogger logger, - IHttpContextAccessor httpContextAccessor, + IUserTokenProvider userTokenProvider, IAppMetadata appMetadata, IDataClient dataClient, IEFormidlingReceivers eFormidlingReceivers, @@ -51,9 +50,9 @@ public DefaultEFormidlingService( { _logger = logger; _tokenGenerator = tokenGenerator; - _httpContextAccessor = httpContextAccessor; _appSettings = appSettings?.Value; _platformSettings = platformSettings?.Value; + _userTokenProvider = userTokenProvider; _eFormidlingClient = eFormidlingClient; _eFormidlingMetadata = eFormidlingMetadata; _appMetadata = appMetadata; @@ -76,7 +75,7 @@ public async Task SendEFormidlingShipment(Instance instance) ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); string accessToken = _tokenGenerator.GenerateAccessToken(applicationMetadata.Org, applicationMetadata.AppIdentifier.App); - string authzToken = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _appSettings.RuntimeCookieName); + string authzToken = _userTokenProvider.GetUserToken(); var requestHeaders = new Dictionary { @@ -114,7 +113,7 @@ public async Task SendEFormidlingShipment(Instance instance) private async Task ConstructStandardBusinessDocument(string instanceGuid, Instance instance) { - DateTime completedTime = DateTime.Now; + DateTime completedTime = DateTime.UtcNow; Sender digdirSender = new Sender { diff --git a/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs b/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs index cfb9028e1..5379c6f0c 100644 --- a/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs +++ b/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs @@ -127,7 +127,7 @@ public async Task> ValidateAndUpdateProcess(Instance insta // The condition for completion is met if there are no errors (or other weirdnesses). CanCompleteTask = messages.Count == 0 || messages.All(m => m.Severity != ValidationIssueSeverity.Error && m.Severity != ValidationIssueSeverity.Unspecified), - Timestamp = DateTime.Now + Timestamp = DateTime.UtcNow }; await _instanceClient.UpdateProcess(instance); diff --git a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs index 9cb282b8b..c646d0add 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs @@ -65,7 +65,7 @@ public async Task Get_ShouldReturnProcessTasks() "altinnTaskType": "data", "ended": null, "validated": { - "timestamp": "2020-02-07T10:46:36.985894+01:00", + "timestamp": "2020-02-07T10:46:36.985894Z", "canCompleteTask": false }, "flowType": null diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500000/5d9e906b-83ed-44df-85a7-2f104c640bff.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500000/5d9e906b-83ed-44df-85a7-2f104c640bff.pretest.json index 69a5be102..407a73f44 100644 --- a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500000/5d9e906b-83ed-44df-85a7-2f104c640bff.pretest.json +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500000/5d9e906b-83ed-44df-85a7-2f104c640bff.pretest.json @@ -16,7 +16,7 @@ "name": "Utfylling", "altinnTaskType": "data", "validated": { - "timestamp": "2020-02-07T10:46:36.985894+01:00", + "timestamp": "2020-02-07T10:46:36.985894Z", "canCompleteTask": false } } diff --git a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs index 9bbc2ab2b..4e591793d 100644 --- a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs @@ -197,7 +197,7 @@ public Task UpdateData(T dataToSerialize, Guid instanceGuid, Typ serializer.Serialize(stream, dataToSerialize); } - dataElement.LastChanged = DateTime.Now; + dataElement.LastChanged = DateTime.UtcNow; WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId); return Task.FromResult(dataElement); diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs new file mode 100644 index 000000000..82b23e264 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs @@ -0,0 +1,220 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Constants; +using Altinn.App.Core.EFormidling; +using Altinn.App.Core.EFormidling.Implementation; +using Altinn.App.Core.EFormidling.Interface; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Events; +using Altinn.App.Core.Models; +using Altinn.Common.AccessTokenClient.Services; +using Altinn.Common.EFormidlingClient; +using Altinn.Common.EFormidlingClient.Models.SBD; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Eformidling.Implementation; + +public class DefaultEFormidlingServiceTests +{ + [Fact] + public void SendEFormidlingShipment() + { + // Arrange + var logger = new NullLogger(); + var userTokenProvider = new Mock(); + var appMetadata = new Mock(); + var dataClient = new Mock(); + var eFormidlingReceivers = new Mock(); + var eventClient = new Mock(); + var appSettings = Options.Create(new AppSettings + { + RuntimeCookieName = "AltinnStudioRuntime", + EFormidlingSender = "980123456", + }); + var platformSettings = Options.Create(new PlatformSettings + { + SubscriptionKey = "subscription-key" + }); + var eFormidlingClient = new Mock(); + var tokenGenerator = new Mock(); + var eFormidlingMetadata = new Mock(); + var instance = new Instance + { + Id = "1337/41C1099C-7EDD-47F5-AD1F-6267B497796F", + InstanceOwner = new InstanceOwner + { + PartyId = "1337", + }, + Data = new List() + }; + + appMetadata.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata("ttd/test-app") + { + Org = "ttd", + EFormidling = new EFormidlingContract + { + Process = "urn:no:difi:profile:arkivmelding:plan:3.0", + Standard = "urn:no:difi:arkivmelding:xsd::arkivmelding", + TypeVersion = "v8", + Type = "arkivmelding", + SecurityLevel = 3, + DataTypes = new List() + } + }); + tokenGenerator.Setup(t => t.GenerateAccessToken("ttd", "test-app")).Returns("access-token"); + userTokenProvider.Setup(u => u.GetUserToken()).Returns("authz-token"); + eFormidlingReceivers.Setup(er => er.GetEFormidlingReceivers(instance)).ReturnsAsync(new List()); + eFormidlingMetadata.Setup(em => em.GenerateEFormidlingMetadata(instance)).ReturnsAsync(() => + { + return ("fakefilename.txt", Stream.Null); + }); + + var defaultEformidlingService = new DefaultEFormidlingService( + logger, + userTokenProvider.Object, + appMetadata.Object, + dataClient.Object, + eFormidlingReceivers.Object, + eventClient.Object, + appSettings, + platformSettings, + eFormidlingClient.Object, + tokenGenerator.Object, + eFormidlingMetadata.Object); + + // Act + var result = defaultEformidlingService.SendEFormidlingShipment(instance); + + // Assert + var expectedReqHeaders = new Dictionary + { + { "Authorization", $"Bearer authz-token" }, + { General.EFormidlingAccessTokenHeaderName, "access-token" }, + { General.SubscriptionKeyHeaderName, "subscription-key" } + }; + + appMetadata.Verify(a => a.GetApplicationMetadata()); + tokenGenerator.Verify(t => t.GenerateAccessToken("ttd", "test-app")); + userTokenProvider.Verify(u => u.GetUserToken()); + eFormidlingReceivers.Verify(er => er.GetEFormidlingReceivers(instance)); + eFormidlingMetadata.Verify(em => em.GenerateEFormidlingMetadata(instance)); + eFormidlingClient.Verify(ec => ec.CreateMessage(It.IsAny(), expectedReqHeaders)); + eFormidlingClient.Verify(ec => ec.UploadAttachment(Stream.Null, "41C1099C-7EDD-47F5-AD1F-6267B497796F", "fakefilename.txt", expectedReqHeaders)); + eFormidlingClient.Verify(ec => ec.SendMessage("41C1099C-7EDD-47F5-AD1F-6267B497796F", expectedReqHeaders)); + eventClient.Verify(e => e.AddEvent(EformidlingConstants.CheckInstanceStatusEventType, instance)); + + eFormidlingClient.VerifyNoOtherCalls(); + eventClient.VerifyNoOtherCalls(); + tokenGenerator.VerifyNoOtherCalls(); + userTokenProvider.VerifyNoOtherCalls(); + eFormidlingReceivers.VerifyNoOtherCalls(); + appMetadata.VerifyNoOtherCalls(); + + result.IsCompletedSuccessfully.Should().BeTrue(); + } + + [Fact] + public void SendEFormidlingShipment_throws_exception_if_send_fails() + { + // Arrange + var logger = new NullLogger(); + var userTokenProvider = new Mock(); + var appMetadata = new Mock(); + var dataClient = new Mock(); + var eFormidlingReceivers = new Mock(); + var eventClient = new Mock(); + var appSettings = Options.Create(new AppSettings + { + RuntimeCookieName = "AltinnStudioRuntime", + EFormidlingSender = "980123456", + }); + var platformSettings = Options.Create(new PlatformSettings + { + SubscriptionKey = "subscription-key", + }); + var eFormidlingClient = new Mock(); + var tokenGenerator = new Mock(); + var eFormidlingMetadata = new Mock(); + var instance = new Instance + { + Id = "1337/41C1099C-7EDD-47F5-AD1F-6267B497796F", + InstanceOwner = new InstanceOwner + { + PartyId = "1337", + }, + Data = new List() + }; + + appMetadata.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(new ApplicationMetadata("ttd/test-app") + { + Org = "ttd", + EFormidling = new EFormidlingContract + { + Process = "urn:no:difi:profile:arkivmelding:plan:3.0", + Standard = "urn:no:difi:arkivmelding:xsd::arkivmelding", + TypeVersion = "v8", + Type = "arkivmelding", + SecurityLevel = 3, + DataTypes = new List() + } + }); + tokenGenerator.Setup(t => t.GenerateAccessToken("ttd", "test-app")).Returns("access-token"); + userTokenProvider.Setup(u => u.GetUserToken()).Returns("authz-token"); + eFormidlingReceivers.Setup(er => er.GetEFormidlingReceivers(instance)).ReturnsAsync(new List()); + eFormidlingMetadata.Setup(em => em.GenerateEFormidlingMetadata(instance)).ReturnsAsync(() => + { + return ("fakefilename.txt", Stream.Null); + }); + eFormidlingClient.Setup(ec => ec.SendMessage(It.IsAny(), It.IsAny>())) + .ThrowsAsync(new Exception("XUnit expected exception")); + + var defaultEformidlingService = new DefaultEFormidlingService( + logger, + userTokenProvider.Object, + appMetadata.Object, + dataClient.Object, + eFormidlingReceivers.Object, + eventClient.Object, + appSettings, + platformSettings, + eFormidlingClient.Object, + tokenGenerator.Object, + eFormidlingMetadata.Object); + + // Act + var result = defaultEformidlingService.SendEFormidlingShipment(instance); + + // Assert + // Assert + var expectedReqHeaders = new Dictionary + { + { "Authorization", $"Bearer authz-token" }, + { General.EFormidlingAccessTokenHeaderName, "access-token" }, + { General.SubscriptionKeyHeaderName, "subscription-key" } + }; + + appMetadata.Verify(a => a.GetApplicationMetadata()); + tokenGenerator.Verify(t => t.GenerateAccessToken("ttd", "test-app")); + userTokenProvider.Verify(u => u.GetUserToken()); + eFormidlingReceivers.Verify(er => er.GetEFormidlingReceivers(instance)); + eFormidlingMetadata.Verify(em => em.GenerateEFormidlingMetadata(instance)); + eFormidlingClient.Verify(ec => ec.CreateMessage(It.IsAny(), expectedReqHeaders)); + eFormidlingClient.Verify(ec => ec.UploadAttachment(Stream.Null, "41C1099C-7EDD-47F5-AD1F-6267B497796F", "fakefilename.txt", expectedReqHeaders)); + eFormidlingClient.Verify(ec => ec.SendMessage("41C1099C-7EDD-47F5-AD1F-6267B497796F", expectedReqHeaders)); + + eFormidlingClient.VerifyNoOtherCalls(); + eventClient.VerifyNoOtherCalls(); + tokenGenerator.VerifyNoOtherCalls(); + userTokenProvider.VerifyNoOtherCalls(); + eFormidlingReceivers.VerifyNoOtherCalls(); + appMetadata.VerifyNoOtherCalls(); + + result.IsCompletedSuccessfully.Should().BeFalse(); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs b/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs index 83b38a9dc..8a275ff2b 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs @@ -7,6 +7,7 @@ using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Internal.Instances; +using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; @@ -14,7 +15,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Moq; using Xunit; @@ -91,15 +91,119 @@ public async Task FileScanEnabled_Clean_ValidationShouldNotFail() validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileScanPending").Should().BeNull(); } - private static ValidationAppSI ConfigureMockServicesForValidation() + [Fact] + public async Task ValidateAndUpdateProcess_set_canComplete_validationstatus_and_return_empty_list() + { + const string taskId = "Task_1"; + + // Mock setup + var appMetadataMock = new Mock(); + var appMetadata = new ApplicationMetadata("ttd/test-app") + { + DataTypes = new List + { + new DataType + { + Id = "data", + TaskId = taskId, + MaxCount = 0, + } + } + }; + appMetadataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(appMetadata); + ValidationAppSI validationAppSI = ConfigureMockServicesForValidation(appMetadataMock.Object); + + // Testdata + var instance = new Instance + { + Data = + [ + new DataElement + { + DataType = "data", + ContentType = "application/json" + }, + ], + Process = new ProcessState + { + CurrentTask = new ProcessElementInfo + { + Name = "Task_1" + } + } + }; + + var issues = await validationAppSI.ValidateAndUpdateProcess(instance, taskId); + issues.Should().BeEmpty(); + instance.Process?.CurrentTask?.Validated.CanCompleteTask.Should().BeTrue(); + instance.Process?.CurrentTask?.Validated.Timestamp.Should().NotBeNull(); + } + + [Fact] + public async Task ValidateAndUpdateProcess_set_canComplete_false_validationstatus_and_return_list_of_issues() + { + const string taskId = "Task_1"; + + // Mock setup + var appMetadataMock = new Mock(); + var appMetadata = new ApplicationMetadata("ttd/test-app") + { + DataTypes = new List + { + new DataType + { + Id = "data", + TaskId = taskId, + MaxCount = 1, + } + } + }; + appMetadataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(appMetadata); + ValidationAppSI validationAppSI = ConfigureMockServicesForValidation(appMetadataMock.Object); + + // Testdata + var instance = new Instance + { + Data = + [ + new DataElement + { + Id = "3C8B52A9-9602-4B2E-A217-B4E816ED8DEB", + DataType = "data", + ContentType = "application/json" + }, + new DataElement + { + Id = "3C8B52A9-9602-4B2E-A217-B4E816ED8DEC", + DataType = "data", + ContentType = "application/json" + }, + ], + Process = new ProcessState + { + CurrentTask = new ProcessElementInfo + { + Name = "Task_1" + } + } + }; + + var issues = await validationAppSI.ValidateAndUpdateProcess(instance, taskId); + issues.Should().HaveCount(1); + issues.Should().ContainSingle(i => i.Code == ValidationIssueCodes.InstanceCodes.TooManyDataElementsOfType); + instance.Process?.CurrentTask?.Validated.CanCompleteTask.Should().BeFalse(); + instance.Process?.CurrentTask?.Validated.Timestamp.Should().NotBeNull(); + } + + private static ValidationAppSI ConfigureMockServicesForValidation(IAppMetadata? appMetadataInput = null, IInstanceValidator? instanceValidatorInput = null) { Mock> loggerMock = new(); var dataMock = new Mock(); var instanceMock = new Mock(); - var instanceValidator = new Mock(); + var instanceValidator = instanceValidatorInput ?? new Mock().Object; var appModelMock = new Mock(); var appResourcesMock = new Mock(); - var appMetadataMock = new Mock(); + var appMetadata = appMetadataInput ?? new Mock().Object; var objectModelValidatorMock = new Mock(); var layoutEvaluatorStateInitializer = new LayoutEvaluatorStateInitializer(appResourcesMock.Object, Microsoft.Extensions.Options.Options.Create(new Configuration.FrontEndSettings())); var httpContextAccessorMock = new Mock(); @@ -110,10 +214,10 @@ private static ValidationAppSI ConfigureMockServicesForValidation() loggerMock.Object, dataMock.Object, instanceMock.Object, - instanceValidator.Object, + instanceValidator, appModelMock.Object, appResourcesMock.Object, - appMetadataMock.Object, + appMetadata, objectModelValidatorMock.Object, layoutEvaluatorStateInitializer, httpContextAccessorMock.Object, From 3bac97d25b3cdbd2f5ea1f4571aa74e82f2522fa Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Mon, 22 Jan 2024 21:36:44 +0100 Subject: [PATCH 44/46] New validation and partial validation via PATCH endpoint (#393) * Revert Multipart form data #329 on PUT endpoint * Add JsonPatch and create a new PATCH endpoint for updating form data * Fix a few SonarCloud warnings * Start work on new validation Improve GetJsonPath(Expression) and fix some sonarcloud issues Improve test coverage Full validation rewrite complete and existing tests work Probably still some issues and test coverage was spoty so more tests needs to be written. Make IInstanceValidator obsolete Add DataType as input to validators Fix a few issues to prepare for tests of expressions Add more tests * Fix sonar cloud issues * Rename to get more consistent naming * Clanup DataType/TaskId to allow * Also comment out KeyedService fetching * Fix more sonarcloud issues * Registrer new validators in DI container * Remove dead code * Adjust interface and add a few tests * Add object? previousData to IDataProcessor * Cleanup and more tests * OptionsSource doesn't really need a label to work. In recent fronted versions, this label can also be an expression, so parsing fails when this is expected to be a string. * Removing the requirement for a 'pageRef' property on Summary components. Frontend doesn't use this anymore, and disallows rendering Summary components with it. * Allowing attachments to be deleted (attachments don't have AppLogic) * ValidationIssue.Severity is an int in json Improve handeling of errors in PATCH endpoint code style fixes * Initialize lists for concistency in frontend after patch also add tests * Fix issues with list initialization and prettify tests * Add test to verify that PATCH initializes arrays (as xml serialization does) * Read json configuration file for api tests * Set empty strings to null in patch endpoint xml serializing does always preserve empty string when using attributes and [XmlText]. * Apply suggestions from code review Co-authored-by: Vemund Gaukstad * Remove caching logic for CanTaskBeEnded * Improve description on validationsource * Rename ShouldRun to HasRelevantChanges fix style issues * Fix dataProcessorRewriter * Add genericDataProcessor * Fix a few codestyle issues * Making some validator sources match those used on the frontend * Fix tests after validator source name change * Adding support for explicit repeating group components (new in frontend v4) --------- Co-authored-by: Ole Martin Handeland Co-authored-by: Vemund Gaukstad --- .../CodeRewriters/IDataProcessorRewriter.cs | 4 +- src/Altinn.App.Api/Altinn.App.Api.csproj | 3 + .../Controllers/DataController.cs | 273 ++++++++--- .../Controllers/InstancesController.cs | 8 +- .../Controllers/ProcessController.cs | 24 +- .../Controllers/StatelessDataController.cs | 23 +- .../Controllers/ValidateController.cs | 24 +- src/Altinn.App.Api/Models/DataPatchRequest.cs | 25 + .../Models/DataPatchResponse.cs | 20 + src/Altinn.App.Core/Altinn.App.Core.csproj | 1 + .../Extensions/ServiceCollectionExtensions.cs | 23 +- .../DataProcessing/GenericDataProcessor.cs | 39 ++ .../FileAnalyzis/FileAnalysisResult.cs | 2 +- .../Features/IDataElementValidator.cs | 37 ++ .../Features/IDataProcessor.cs | 4 +- .../Features/IFormDataValidator.cs | 42 ++ .../Features/IInstanceValidator.cs | 2 + .../Features/ITaskValidator.cs | 41 ++ .../Default/DataAnnotationValidator.cs | 69 +++ .../Default/DefaultDataElementValidator.cs | 90 ++++ .../Default/DefaultTaskValidator.cs | 64 +++ .../Validation/Default/ExpressionValidator.cs | 303 ++++++++++++ ...gacyIInstanceValidatorFormDataValidator.cs | 51 +++ .../LegacyIInstanceValidatorTaskValidator.cs | 49 ++ .../Validation/Default/RequiredValidator.cs | 52 +++ .../Validation/ExpressionValidator.cs | 276 ----------- .../Validation/GenericFormDataValidator.cs | 109 +++++ .../Validation/Helpers/ModelStateHelpers.cs | 174 +++++++ .../Features/Validation/IValidation.cs | 28 -- .../Features/Validation/IValidationService.cs | 54 +++ .../Validation/NullInstanceValidator.cs | 23 - .../Features/Validation/ValidationAppSI.cs | 433 ------------------ .../Features/Validation/ValidationService.cs | 156 +++++++ src/Altinn.App.Core/Helpers/JsonHelper.cs | 4 +- .../Helpers/LinqExpressionHelpers.cs | 83 ++++ src/Altinn.App.Core/Helpers/ObjectUtils.cs | 54 +++ .../Serialization/ModelDeserializer.cs | 101 ++-- .../Serialization/ModelDeserializerResult.cs | 64 --- .../Clients/Storage/DataClient.cs | 12 +- .../Internal/Expressions/LayoutEvaluator.cs | 3 +- .../Expressions/LayoutEvaluatorState.cs | 8 + .../LayoutEvaluatorStateInitializer.cs | 2 +- .../Layout/Components/OptionsComponent.cs | 33 +- .../Layout/Components/SummaryComponent.cs | 8 +- .../Models/Layout/PageComponentConverter.cs | 25 +- .../Models/Validation/ValidationIssue.cs | 51 ++- .../Validation/ValidationIssueSeverity.cs | 1 + ...aController_PatchFormDataImplementation.cs | 156 +++++++ .../Controllers/DataController_PatchTests.cs | 343 ++++++++++++++ .../Controllers/DataController_PutTests.cs | 81 +--- ...nstancesController_ActiveInstancesTests.cs | 4 +- .../Controllers/ValidateControllerTests.cs | 19 +- .../ValidateControllerValidateDataTests.cs | 12 +- ...alidateController_ValidateInstanceTests.cs | 138 ++++++ .../tdd/contributer-restriction/.gitignore | 10 +- ...3-fe31-4ef5-8fb9-dd3f479354cd.pretest.json | 30 ++ ...121812-0336-45fb-a75c-490df3ad5109.pretest | 6 + ...2-0336-45fb-a75c-490df3ad5109.pretest.json | 22 + ...d-1446-4ca5-9fed-3c7c7d67249c.pretest.json | 30 ++ ...4-dca6-44d3-b99a-1b7ca9b862af.pretest.json | 22 + ...40d834-dca6-44d3-b99a-1b7ca9b862af.pretest | 7 + .../contributer-restriction/appsettings.json | 4 +- .../contributer-restriction/models/Skjema.cs | 19 + .../contributer-restriction/ui/Settings.json | 8 + .../ui/layouts/page.json | 23 + test/Altinn.App.Api.Tests/Program.cs | 3 + .../Default/DataAnnotationValidatorTests.cs | 194 ++++++++ .../Default/DefaultTaskValidatorTests.cs | 137 ++++++ .../Default/ExpressionValidatorTests.cs | 125 +++++ .../Default/LegacyIValidationFormDataTests.cs | 138 ++++++ .../Validators/ExpressionValidationTests.cs | 33 +- .../Validators/GenericValidatorTests.cs | 74 +++ ...ITests.cs => ValidationServiceOldTests.cs} | 198 ++++---- .../Validators/ValidationServiceTests.cs | 116 +++++ .../Helpers/JsonHelperTests.cs | 6 +- .../Helpers/LinqExpressionHelpersTests.cs | 68 +++ .../Helpers/ModelDeserializerTests.cs | 144 ------ .../Helpers/ObjectUtilsTests.cs | 105 +++++ .../NullInstanceValidatorTests.cs | 38 -- .../FullTests/Test2/SecondPage.json | 3 +- .../FullTests/Test3/SecondPage.json | 3 +- 81 files changed, 3846 insertions(+), 1448 deletions(-) create mode 100644 src/Altinn.App.Api/Models/DataPatchRequest.cs create mode 100644 src/Altinn.App.Api/Models/DataPatchResponse.cs create mode 100644 src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs create mode 100644 src/Altinn.App.Core/Features/IDataElementValidator.cs create mode 100644 src/Altinn.App.Core/Features/IFormDataValidator.cs create mode 100644 src/Altinn.App.Core/Features/ITaskValidator.cs create mode 100644 src/Altinn.App.Core/Features/Validation/Default/DataAnnotationValidator.cs create mode 100644 src/Altinn.App.Core/Features/Validation/Default/DefaultDataElementValidator.cs create mode 100644 src/Altinn.App.Core/Features/Validation/Default/DefaultTaskValidator.cs create mode 100644 src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs create mode 100644 src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorFormDataValidator.cs create mode 100644 src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs create mode 100644 src/Altinn.App.Core/Features/Validation/Default/RequiredValidator.cs delete mode 100644 src/Altinn.App.Core/Features/Validation/ExpressionValidator.cs create mode 100644 src/Altinn.App.Core/Features/Validation/GenericFormDataValidator.cs create mode 100644 src/Altinn.App.Core/Features/Validation/Helpers/ModelStateHelpers.cs delete mode 100644 src/Altinn.App.Core/Features/Validation/IValidation.cs create mode 100644 src/Altinn.App.Core/Features/Validation/IValidationService.cs delete mode 100644 src/Altinn.App.Core/Features/Validation/NullInstanceValidator.cs delete mode 100644 src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs create mode 100644 src/Altinn.App.Core/Features/Validation/ValidationService.cs create mode 100644 src/Altinn.App.Core/Helpers/LinqExpressionHelpers.cs create mode 100644 src/Altinn.App.Core/Helpers/ObjectUtils.cs delete mode 100644 src/Altinn.App.Core/Helpers/Serialization/ModelDeserializerResult.cs create mode 100644 test/Altinn.App.Api.Tests/Controllers/DataController_PatchFormDataImplementation.cs create mode 100644 test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs create mode 100644 test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd.pretest.json create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/blob/fc121812-0336-45fb-a75c-490df3ad5109.pretest create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/fc121812-0336-45fb-a75c-490df3ad5109.pretest.json create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c.pretest.json create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest.json create mode 100644 test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/blob/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/Settings.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/layouts/page.json create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/Default/DataAnnotationValidatorTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/Default/DefaultTaskValidatorTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/Default/ExpressionValidatorTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/Default/LegacyIValidationFormDataTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/GenericValidatorTests.cs rename test/Altinn.App.Core.Tests/Features/Validators/{ValidationAppSITests.cs => ValidationServiceOldTests.cs} (54%) create mode 100644 test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs create mode 100644 test/Altinn.App.Core.Tests/Helpers/LinqExpressionHelpersTests.cs delete mode 100644 test/Altinn.App.Core.Tests/Helpers/ModelDeserializerTests.cs create mode 100644 test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs delete mode 100644 test/Altinn.App.Core.Tests/Implementation/NullInstanceValidatorTests.cs diff --git a/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/IDataProcessorRewriter.cs b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/IDataProcessorRewriter.cs index b73916f00..5b1081666 100644 --- a/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/IDataProcessorRewriter.cs +++ b/cli-tools/altinn-app-cli/v7Tov8/CodeRewriters/IDataProcessorRewriter.cs @@ -62,9 +62,9 @@ private MethodDeclarationSyntax Update_DataProcessWrite(MethodDeclarationSyntax private MethodDeclarationSyntax AddParameter_ChangedFields(MethodDeclarationSyntax method) { return method.ReplaceNode(method.ParameterList, - method.ParameterList.AddParameters(SyntaxFactory.Parameter(SyntaxFactory.Identifier("changedFields")) + method.ParameterList.AddParameters(SyntaxFactory.Parameter(SyntaxFactory.Identifier("previousData")) .WithLeadingTrivia(SyntaxFactory.Space) - .WithType(SyntaxFactory.ParseTypeName("System.Collections.Generic.Dictionary?")) + .WithType(SyntaxFactory.ParseTypeName("object?")) .WithLeadingTrivia(SyntaxFactory.Space))); } diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index decbd1d50..bebf83d1d 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -48,4 +48,7 @@ $(NoWarn);1591 + + + diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index eac7f46be..7353c8e10 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -1,9 +1,15 @@ #nullable enable +using System.Collections; using System.Net; +using System.Reflection; using System.Security.Claims; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using Altinn.App.Api.Helpers.RequestHandling; using Altinn.App.Api.Infrastructure.Filters; +using Altinn.App.Api.Models; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; @@ -20,6 +26,7 @@ using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; +using Json.Patch; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; @@ -46,9 +53,17 @@ public class DataController : ControllerBase private readonly IAppResources _appResourcesService; private readonly IAppMetadata _appMetadata; private readonly IPrefill _prefillService; + private readonly IValidationService _validationService; private readonly IFileAnalysisService _fileAnalyserService; private readonly IFileValidationService _fileValidationService; private readonly IFeatureManager _featureManager; + + private static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + PropertyNameCaseInsensitive = true, + }; + private const long REQUEST_SIZE_LIMIT = 2000 * 1024 * 1024; /// @@ -64,6 +79,7 @@ public class DataController : ControllerBase /// The app metadata service /// The feature manager controlling enabled features. /// A service with prefill related logic. + /// The service used to validate data /// Service used to analyse files uploaded. /// Service used to validate files uploaded. public DataController( @@ -75,6 +91,7 @@ public DataController( IAppModel appModel, IAppResources appResourcesService, IPrefill prefillService, + IValidationService validationService, IFileAnalysisService fileAnalyserService, IFileValidationService fileValidationService, IAppMetadata appMetadata, @@ -90,6 +107,7 @@ public DataController( _appResourcesService = appResourcesService; _appMetadata = appMetadata; _prefillService = prefillService; + _validationService = validationService; _fileAnalyserService = fileAnalyserService; _fileValidationService = fileValidationService; _featureManager = featureManager; @@ -122,7 +140,7 @@ public async Task Create( { Application application = await _appMetadata.GetApplicationMetadata(); - DataType? dataTypeFromMetadata = application.DataTypes.FirstOrDefault(e => e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase)); + DataType? dataTypeFromMetadata = application.DataTypes.First(e => e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase)); if (dataTypeFromMetadata == null) { @@ -256,24 +274,22 @@ public async Task Get( return NotFound($"Did not find instance {instance}"); } - DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { return NotFound("Did not find data element"); } - string dataType = dataElement.DataType; - - bool? appLogic = await RequiresAppLogic(dataType); + DataType? dataType = await GetDataType(dataElement); - if (appLogic == null) + if (dataType is null) { string error = $"Could not determine if {dataType} requires app logic for application {org}/{app}"; _logger.LogError(error); return BadRequest(error); } - else if ((bool)appLogic) + else if (dataType.AppLogic?.ClassRef is not null) { return await GetFormData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, dataType, instance); } @@ -317,29 +333,26 @@ public async Task Put( return Conflict($"Cannot update data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}"); } - DataElement? dataElement = instance.Data.FirstOrDefault(m => m.Id.Equals(dataGuid.ToString())); + DataElement? dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString())); if (dataElement == null) { return NotFound("Did not find data element"); } - string dataType = dataElement.DataType; + DataType? dataType = await GetDataType(dataElement); - bool? appLogic = await RequiresAppLogic(dataType); - - if (appLogic == null) + if (dataType is null) { _logger.LogError("Could not determine if {dataType} requires app logic for application {org}/{app}", dataType, org, app); return BadRequest($"Could not determine if data type {dataType} requires application logic."); } - else if (appLogic == true) + else if (dataType.AppLogic?.ClassRef is not null) { return await PutFormData(org, app, instance, dataGuid, dataType); } - DataType? dataTypeFromMetadata = (await _appMetadata.GetApplicationMetadata()).DataTypes.FirstOrDefault(e => e.Id.Equals(dataType, StringComparison.InvariantCultureIgnoreCase)); - (bool validationRestrictionSuccess, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataTypeFromMetadata); + (bool validationRestrictionSuccess, List errors) = DataRestrictionValidation.CompliesWithDataRestrictions(Request, dataType); if (!validationRestrictionSuccess) { return BadRequest(await GetErrorDetails(errors)); @@ -353,6 +366,172 @@ public async Task Put( } } + /// + /// Updates an existing form data element with a patch of changes. + /// + /// unique identfier of the organisation responsible for the app + /// application identifier which is unique within an organisation + /// unique id of the party that is the owner of the instance + /// unique id to identify the instance + /// unique id to identify the data element to update + /// Container object for the and list of ignored validators + /// A response object with the new full model and validation issues from all the groups that run + [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] + [HttpPatch("{dataGuid:guid}")] + [ProducesResponseType(typeof(DataPatchResponse), 200)] + [ProducesResponseType(typeof(ProblemDetails), 412)] + [ProducesResponseType(typeof(ProblemDetails), 422)] + public async Task> PatchFormData( + [FromRoute] string org, + [FromRoute] string app, + [FromRoute] int instanceOwnerPartyId, + [FromRoute] Guid instanceGuid, + [FromRoute] Guid dataGuid, + [FromBody] DataPatchRequest dataPatchRequest) + { + try + { + var instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid); + + if (!InstanceIsActive(instance)) + { + return Conflict( + $"Cannot update data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}"); + } + + var dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString())); + + if (dataElement == null) + { + return NotFound("Did not find data element"); + } + + var dataType = await GetDataType(dataElement); + + if (dataType?.AppLogic?.ClassRef is null) + { + _logger.LogError( + "Could not determine if {dataType} requires app logic for application {org}/{app}", + dataType, + org, + app); + return BadRequest($"Could not determine if data type {dataType?.Id} requires application logic."); + } + + var modelType = _appModel.GetModelType(dataType.AppLogic.ClassRef); + + var oldModel = + await _dataClient.GetFormData(instanceGuid, modelType, org, app, instanceOwnerPartyId, dataGuid); + + var (response, problemDetails) = + await PatchFormDataImplementation(dataType, dataElement, dataPatchRequest, oldModel, instance); + + if (problemDetails is not null) + { + return StatusCode(problemDetails.Status ?? 500, problemDetails); + } + + await UpdatePresentationTextsOnInstance(instance, dataType.Id, response.NewDataModel); + await UpdateDataValuesOnInstance(instance, dataType.Id, response.NewDataModel); + + // Save Formdata to database + await _dataClient.UpdateData( + response.NewDataModel, + instanceGuid, + modelType, + org, + app, + instanceOwnerPartyId, + dataGuid); + + return Ok(response); + } + catch (PlatformHttpException e) + { + return HandlePlatformHttpException(e, $"Unable to update data element {dataGuid} for instance {instanceOwnerPartyId}/{instanceGuid}"); + } + } + + /// + /// Part of that is separated out for testing purposes. + /// + /// The type of the data element + /// The data element + /// Container object for the and list of ignored validators + /// The old state of the form data + /// The instance + /// DataPatchResponse after this patch operation + internal async Task<(DataPatchResponse Response, ProblemDetails? Error)> PatchFormDataImplementation(DataType dataType, DataElement dataElement, DataPatchRequest dataPatchRequest, object oldModel, Instance instance) + { + var oldModelNode = JsonSerializer.SerializeToNode(oldModel); + var patchResult = dataPatchRequest.Patch.Apply(oldModelNode); + if (!patchResult.IsSuccess) + { + bool testOperationFailed = patchResult.Error!.Contains("is not equal to the indicated value."); + return (null!, new ProblemDetails() + { + Title = testOperationFailed ? "Precondition in patch failed" : "Patch Operation Failed", + Detail = patchResult.Error, + Type = "https://datatracker.ietf.org/doc/html/rfc6902/", + Status = testOperationFailed ? (int)HttpStatusCode.PreconditionFailed : (int)HttpStatusCode.UnprocessableContent, + Extensions = new Dictionary() + { + { "previousModel", oldModel }, + { "patchOperationIndex", patchResult.Operation }, + } + }); + } + + var (model, error) = DeserializeModel(oldModel.GetType(), patchResult.Result!); + if (error is not null) + { + return (null!, new ProblemDetails() + { + Title = "Patch operation did not deserialize", + Detail = error, + Type = "https://datatracker.ietf.org/doc/html/rfc6902/", + Status = (int)HttpStatusCode.UnprocessableContent, + }); + } + + foreach (var dataProcessor in _dataProcessors) + { + await dataProcessor.ProcessDataWrite(instance, Guid.Parse(dataElement.Id), model, oldModel); + } + + // Ensure that all lists are changed from null to empty list. + ObjectUtils.InitializeListsAndNullEmptyStrings(model); + + var changedFields = dataPatchRequest.Patch.Operations.Select(o => o.Path.ToString()).ToList(); + + var validationIssues = await _validationService.ValidateFormData(instance, dataElement, dataType, model, changedFields, dataPatchRequest.IgnoredValidators); + var response = new DataPatchResponse + { + NewDataModel = model, + ValidationIssues = validationIssues + }; + return (response, null); + } + + private static (object Model, string? Error) DeserializeModel(Type type, JsonNode patchResult) + { + try + { + var model = patchResult.Deserialize(type, JsonSerializerOptions); + if (model is null) + { + return (null!, "Deserialize patched model returned null"); + } + + return (model, null); + } + catch (JsonException e) when (e.Message.Contains("could not be mapped to any .NET member contained in type")) + { + // Give better feedback when the issue is that the patch contains a path that does not exist in the model + return (null!, e.Message); + } + } + /// /// Delete a data element. /// @@ -391,17 +570,15 @@ public async Task Delete( return NotFound("Did not find data element"); } - string dataType = dataElement.DataType; + DataType? dataType = await GetDataType(dataElement); - bool? appLogic = await RequiresAppLogic(dataType); - - if (appLogic == null) + if (dataType == null) { - string errorMsg = $"Could not determine if {dataType} requires app logic for application {org}/{app}"; + string errorMsg = $"Could not determine if {dataElement.DataType} requires app logic for application {org}/{app}"; _logger.LogError(errorMsg); return BadRequest(errorMsg); } - else if ((bool)appLogic) + else if (dataType.AppLogic?.ClassRef is not null) { // trying deleting a form element return BadRequest("Deleting form data is not possible at this moment."); @@ -466,14 +643,12 @@ private async Task CreateAppModelData( else { ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (deserializerResult.HasError) + if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) { - return BadRequest(deserializerResult.Error); + return BadRequest(deserializer.Error); } - - appModel = deserializerResult.Model; } // runs prefill from repo configuration if config exists @@ -536,21 +711,10 @@ private async Task DeleteBinaryData(string org, string app, int in } } - private async Task RequiresAppLogic(string dataType) + private async Task GetDataType(DataElement element) { - bool? appLogic = false; - - try - { - Application application = await _appMetadata.GetApplicationMetadata(); - appLogic = application?.DataTypes.Where(e => e.Id == dataType).Select(e => e.AppLogic?.ClassRef != null).First(); - } - catch (Exception) - { - appLogic = null; - } - - return appLogic; + Application application = await _appMetadata.GetApplicationMetadata(); + return application?.DataTypes.Find(e => e.Id == element.DataType); } /// @@ -565,15 +729,13 @@ private async Task GetFormData( int instanceOwnerId, Guid instanceGuid, Guid dataGuid, - string dataType, + DataType dataType, Instance instance) { - string appModelclassRef = _appResourcesService.GetClassRefForLogicDataType(dataType); - // Get Form Data from data service. Assumes that the data element is form data. object appModel = await _dataClient.GetFormData( instanceGuid, - _appModel.GetModelType(appModelclassRef), + _appModel.GetModelType(dataType.AppLogic.ClassRef), org, app, instanceOwnerId, @@ -614,29 +776,34 @@ private async Task PutBinaryData(int instanceOwnerPartyId, Guid in return BadRequest("Invalid data provided. Error: The request must include a Content-Disposition header"); } - private async Task PutFormData(string org, string app, Instance instance, Guid dataGuid, string dataType) + private async Task PutFormData(string org, string app, Instance instance, Guid dataGuid, DataType dataType) { int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); - string classRef = _appResourcesService.GetClassRefForLogicDataType(dataType); + string classRef = dataType.AppLogic.ClassRef; Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + object? serviceModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + + if (!string.IsNullOrEmpty(deserializer.Error)) + { + return BadRequest(deserializer.Error); + } - if (deserializerResult.HasError) + if (serviceModel == null) { - return BadRequest(deserializerResult.Error); + return BadRequest("No data found in content"); } - Dictionary? changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, deserializerResult.Model, _dataProcessors, deserializerResult.ReportedChanges, _logger); + Dictionary? changedFields = await JsonHelper.ProcessDataWriteWithDiff(instance, dataGuid, serviceModel, _dataProcessors, _logger); - await UpdatePresentationTextsOnInstance(instance, dataType, deserializerResult.Model); - await UpdateDataValuesOnInstance(instance, dataType, deserializerResult.Model); + await UpdatePresentationTextsOnInstance(instance, dataType.Id, serviceModel); + await UpdateDataValuesOnInstance(instance, dataType.Id, serviceModel); // Save Formdata to database DataElement updatedDataElement = await _dataClient.UpdateData( - deserializerResult.Model, + serviceModel, instanceGuid, _appModel.GetModelType(classRef), org, diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index cac36b92f..c6fbe0f87 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -971,15 +971,13 @@ private async Task StorePrefillParts(Instance instance, ApplicationMetadata appI } ModelDeserializer deserializer = new ModelDeserializer(_logger, type); - ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(part.Stream, part.ContentType); + object? data = await deserializer.DeserializeAsync(part.Stream, part.ContentType); - if (deserializerResult.HasError) + if (!string.IsNullOrEmpty(deserializer.Error) || data is null) { - throw new InvalidOperationException(deserializerResult.Error); + throw new InvalidOperationException(deserializer.Error); } - object data = deserializerResult.Model; - await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, part.Name!, data); await _instantiationProcessor.DataCreation(instance, data, null); diff --git a/src/Altinn.App.Api/Controllers/ProcessController.cs b/src/Altinn.App.Api/Controllers/ProcessController.cs index d207a2560..a8959ff0d 100644 --- a/src/Altinn.App.Api/Controllers/ProcessController.cs +++ b/src/Altinn.App.Api/Controllers/ProcessController.cs @@ -2,6 +2,7 @@ using System.Net; using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Models; +using Altinn.App.Core.Features; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Instances; @@ -34,7 +35,7 @@ public class ProcessController : ControllerBase private readonly ILogger _logger; private readonly IInstanceClient _instanceClient; private readonly IProcessClient _processClient; - private readonly IValidation _validationService; + private readonly IValidationService _validationService; private readonly IAuthorizationService _authorization; private readonly IProcessEngine _processEngine; private readonly IProcessReader _processReader; @@ -46,7 +47,7 @@ public ProcessController( ILogger logger, IInstanceClient instanceClient, IProcessClient processClient, - IValidation validationService, + IValidationService validationService, IAuthorizationService authorization, IProcessReader processReader, IProcessEngine processEngine) @@ -202,24 +203,11 @@ public async Task>> GetNextElements( } } - private async Task CanTaskBeEnded(Instance instance, string currentElementId) + private async Task CanTaskBeEnded(Instance instance, string currentTaskId) { - List validationIssues = new List(); + var validationIssues = await _validationService.ValidateInstanceAtTask(instance, currentTaskId); - bool canEndTask; - - if (instance.Process?.CurrentTask?.Validated == null || !instance.Process.CurrentTask.Validated.CanCompleteTask) - { - validationIssues = await _validationService.ValidateAndUpdateProcess(instance, currentElementId); - - canEndTask = await ProcessHelper.CanEndProcessTask(instance, validationIssues); - } - else - { - canEndTask = await ProcessHelper.CanEndProcessTask(instance, validationIssues); - } - - return canEndTask; + return await ProcessHelper.CanEndProcessTask(instance, validationIssues); } /// diff --git a/src/Altinn.App.Api/Controllers/StatelessDataController.cs b/src/Altinn.App.Api/Controllers/StatelessDataController.cs index 9a696e031..49f458953 100644 --- a/src/Altinn.App.Api/Controllers/StatelessDataController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessDataController.cs @@ -116,12 +116,12 @@ public async Task Get( await _prefillService.PrefillDataModel(owner.PartyId, dataType, appModel); Instance virtualInstance = new Instance() { InstanceOwner = owner }; - await ProcessAllDataWrite(virtualInstance, appModel); + await ProcessAllDataRead(virtualInstance, appModel); return Ok(appModel); } - private async Task ProcessAllDataWrite(Instance virtualInstance, object appModel) + private async Task ProcessAllDataRead(Instance virtualInstance, object appModel) { foreach (var dataProcessor in _dataProcessors) { @@ -161,7 +161,7 @@ public async Task GetAnonymous([FromQuery] string dataType) object appModel = _appModel.Create(classRef); var virtualInstance = new Instance(); - await ProcessAllDataWrite(virtualInstance, appModel); + await ProcessAllDataRead(virtualInstance, appModel); return Ok(appModel); } @@ -213,15 +213,13 @@ public async Task Post( } ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (deserializerResult.HasError) + if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) { - return BadRequest(deserializerResult.Error); + return BadRequest(deserializer.Error); } - object appModel = deserializerResult.Model; - // runs prefill from repo configuration if config exists await _prefillService.PrefillDataModel(owner.PartyId, dataType, appModel); @@ -262,22 +260,21 @@ public async Task PostAnonymous([FromQuery] string dataType) } ModelDeserializer deserializer = new ModelDeserializer(_logger, _appModel.GetModelType(classRef)); - ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); + object? appModel = await deserializer.DeserializeAsync(Request.Body, Request.ContentType); - if (deserializerResult.HasError) + if (!string.IsNullOrEmpty(deserializer.Error) || appModel is null) { - return BadRequest(deserializerResult.Error); + return BadRequest(deserializer.Error); } Instance virtualInstance = new Instance(); - var appModel = deserializerResult.Model; foreach (var dataProcessor in _dataProcessors) { _logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", appModel.GetType().Name, dataProcessor.GetType().Name); await dataProcessor.ProcessDataRead(virtualInstance, null, appModel); } - return Ok(deserializerResult.Model); + return Ok(appModel); } private async Task GetInstanceOwner(string? partyFromHeader) diff --git a/src/Altinn.App.Api/Controllers/ValidateController.cs b/src/Altinn.App.Api/Controllers/ValidateController.cs index 3c376af73..5646edeef 100644 --- a/src/Altinn.App.Api/Controllers/ValidateController.cs +++ b/src/Altinn.App.Api/Controllers/ValidateController.cs @@ -1,8 +1,8 @@ #nullable enable +using Altinn.App.Core.Features; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; -using Altinn.App.Core.Infrastructure.Clients; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Models.Validation; @@ -21,14 +21,14 @@ public class ValidateController : ControllerBase { private readonly IInstanceClient _instanceClient; private readonly IAppMetadata _appMetadata; - private readonly IValidation _validationService; + private readonly IValidationService _validationService; /// /// Initialises a new instance of the class /// public ValidateController( IInstanceClient instanceClient, - IValidation validationService, + IValidationService validationService, IAppMetadata appMetadata) { _instanceClient = instanceClient; @@ -66,7 +66,7 @@ public async Task ValidateInstance( try { - List messages = await _validationService.ValidateAndUpdateProcess(instance, taskId); + List messages = await _validationService.ValidateInstanceAtTask(instance, taskId); return Ok(messages); } catch (PlatformHttpException exception) @@ -81,8 +81,7 @@ public async Task ValidateInstance( } /// - /// Validate an app instance. This will validate all individual data elements, both the binary elements and the elements bound - /// to a model, and then finally the state of the instance. + /// Validate an app instance. This will validate a single data element /// /// Unique identifier of the organisation responsible for the app. /// Application identifier which is unique within an organisation @@ -104,9 +103,6 @@ public async Task ValidateData( return NotFound(); } - // Todo. Figure out where to get this from - Dictionary> serviceText = new Dictionary>(); - if (instance.Process?.CurrentTask?.ElementId == null) { throw new ValidationException("Unable to validate instance without a started process."); @@ -130,19 +126,21 @@ public async Task ValidateData( throw new ValidationException("Unknown element type."); } - messages.AddRange(await _validationService.ValidateDataElement(instance, dataType, element)); + messages.AddRange(await _validationService.ValidateDataElement(instance, element, dataType)); string taskId = instance.Process.CurrentTask.ElementId; + + // Should this be a BadRequest instead? if (!dataType.TaskId.Equals(taskId, StringComparison.OrdinalIgnoreCase)) { ValidationIssue message = new ValidationIssue { Code = ValidationIssueCodes.DataElementCodes.DataElementValidatedAtWrongTask, - InstanceId = instance.Id, Severity = ValidationIssueSeverity.Warning, DataElementId = element.Id, - Description = AppTextHelper.GetAppText( - ValidationIssueCodes.DataElementCodes.DataElementValidatedAtWrongTask, serviceText, null, "nb") + Description = $"Data element for task {dataType.TaskId} validated while currentTask is {taskId}", + CustomTextKey = ValidationIssueCodes.DataElementCodes.DataElementValidatedAtWrongTask, + CustomTextParams = new List() { dataType.TaskId, taskId }, }; messages.Add(message); } diff --git a/src/Altinn.App.Api/Models/DataPatchRequest.cs b/src/Altinn.App.Api/Models/DataPatchRequest.cs new file mode 100644 index 000000000..74a4f7990 --- /dev/null +++ b/src/Altinn.App.Api/Models/DataPatchRequest.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.Text.Json.Serialization; +using Altinn.App.Api.Controllers; +using Json.Patch; + +namespace Altinn.App.Api.Models; + +/// +/// Represents the request to patch data on the . +/// +public class DataPatchRequest +{ + /// + /// The Patch operation to perform. + /// + [JsonPropertyName("patch")] + public required JsonPatch Patch { get; init; } + + /// + /// List of validators to ignore during the patch operation. + /// Issues from these validators will not be run during the save operation, but the validator will run on process/next + /// + [JsonPropertyName("ignoredValidators")] + public required List? IgnoredValidators { get; init; } +} \ No newline at end of file diff --git a/src/Altinn.App.Api/Models/DataPatchResponse.cs b/src/Altinn.App.Api/Models/DataPatchResponse.cs new file mode 100644 index 000000000..cab836e29 --- /dev/null +++ b/src/Altinn.App.Api/Models/DataPatchResponse.cs @@ -0,0 +1,20 @@ +using Altinn.App.Api.Controllers; +using Altinn.App.Core.Models.Validation; + +namespace Altinn.App.Api.Models; + +/// +/// Represents the response from a data patch operation on the . +/// +public class DataPatchResponse +{ + /// + /// The validation issues that were found during the patch operation. + /// + public required Dictionary> ValidationIssues { get; init; } + + /// + /// The current data model after the patch operation. + /// + public required object NewDataModel { get; init; } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index e31d41506..9393d57d3 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 7b91c4794..67bc5f683 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ using Altinn.App.Core.Features.PageOrder; using Altinn.App.Core.Features.Pdf; using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Features.Validation.Default; using Altinn.App.Core.Implementation; using Altinn.App.Core.Infrastructure.Clients.Authentication; using Altinn.App.Core.Infrastructure.Clients.Authorization; @@ -133,7 +134,7 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati { // Services for Altinn App services.TryAddTransient(); - services.TryAddTransient(); + AddValidationServices(services, configuration); services.TryAddTransient(); services.TryAddTransient(); services.TryAddSingleton(); @@ -146,7 +147,6 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati #pragma warning restore CS0618, CS0612 // Type or member is obsolete services.TryAddTransient(); services.TryAddTransient(); - services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); @@ -178,6 +178,25 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati } } + private static void AddValidationServices(IServiceCollection services, IConfiguration configuration) + { + services.TryAddTransient(); + if (configuration.GetSection("AppSettings").Get()?.RequiredValidation == true) + { + services.TryAddTransient(); + } + + if (configuration.GetSection("AppSettings").Get()?.ExpressionValidation == true) + { + services.TryAddTransient(); + } + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + } + /// /// Checks if a service is already added to the collection. /// diff --git a/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs b/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs new file mode 100644 index 000000000..6cfeb8171 --- /dev/null +++ b/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs @@ -0,0 +1,39 @@ +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.DataProcessing; + +/// +/// Convenience class for implementing for a specific model type. +/// +public abstract class GenericDataProcessor : IDataProcessor where TModel : class +{ + /// + /// Do changes to the model after it has been read from storage, but before it is returned to the app. + /// this only executes on page load and not for subsequent updates. + /// + public abstract Task ProcessDataRead(Instance instance, Guid? dataId, TModel model); + + /// + /// Do changes to the model before it is written to storage, and report back to frontend. + /// Tyipically used to add calculated values to the model. + /// + public abstract Task ProcessDataWrite(Instance instance, Guid? dataId, TModel model, TModel? previousModel); + + /// + public async Task ProcessDataRead(Instance instance, Guid? dataId, object data) + { + if (data is TModel model) + { + await ProcessDataRead(instance, dataId, model); + } + } + + /// + public async Task ProcessDataWrite(Instance instance, Guid? dataId, object data, object? previousData) + { + if (data is TModel model) + { + await ProcessDataWrite(instance, dataId, model, previousData as TModel); + } + } +} diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs index 6bbf62d56..279cd97d3 100644 --- a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs +++ b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs @@ -37,7 +37,7 @@ public FileAnalysisResult(string analyserId) public string? MimeType { get; set; } /// - /// Key/Value pairs contaning fining from the analysis. + /// Key/Value pairs containing findings from the analysis. /// public IDictionary Metadata { get; private set; } = new Dictionary(); } diff --git a/src/Altinn.App.Core/Features/IDataElementValidator.cs b/src/Altinn.App.Core/Features/IDataElementValidator.cs new file mode 100644 index 000000000..b05b4f8eb --- /dev/null +++ b/src/Altinn.App.Core/Features/IDataElementValidator.cs @@ -0,0 +1,37 @@ +using Altinn.App.Core.Features.FileAnalysis; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation; + +/// +/// Validator for data elements. +/// See for an alternative validator for data elements with app logic. +/// and that support incremental validation on save. +/// For validating the content of files, see and +/// +public interface IDataElementValidator +{ + /// + /// The data type that this validator should run for. This is the id of the data type from applicationmetadata.json + /// + /// + /// + /// + string DataType { get; } + + /// + /// Returns the group id of the validator. + /// The default is based on the FullName and DataType fields, and should not need customization + /// + string ValidationSource => $"{this.GetType().FullName}-{DataType}"; + + /// + /// Run validations for a data element. This is supposed to run quickly + /// + /// The instance to validate + /// + /// + /// + public Task> ValidateDataElement(Instance instance, DataElement dataElement, DataType dataType); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/IDataProcessor.cs b/src/Altinn.App.Core/Features/IDataProcessor.cs index 270ad65c3..0c1825376 100644 --- a/src/Altinn.App.Core/Features/IDataProcessor.cs +++ b/src/Altinn.App.Core/Features/IDataProcessor.cs @@ -21,6 +21,6 @@ public interface IDataProcessor /// Instance that data belongs to /// Data id for the data (nullable if stateless) /// The data to perform calculations on - /// optional dictionary of field keys and previous values (untrusted from frontend) - public Task ProcessDataWrite(Instance instance, Guid? dataId, object data, Dictionary? changedFields); + /// The previous data model (for running comparisons) + public Task ProcessDataWrite(Instance instance, Guid? dataId, object data, object? previousData); } \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/IFormDataValidator.cs b/src/Altinn.App.Core/Features/IFormDataValidator.cs new file mode 100644 index 000000000..d6c4da3f6 --- /dev/null +++ b/src/Altinn.App.Core/Features/IFormDataValidator.cs @@ -0,0 +1,42 @@ +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Altinn.App.Core.Features; + +/// +/// Interface for handling validation of form data. +/// (i.e. dataElements with AppLogic defined +/// +public interface IFormDataValidator +{ + /// + /// The data type this validator is for. Typically either hard coded by implementation or + /// or set by constructor using a and a keyed service. + /// + /// To validate all types with form data, just use a "*" as value + /// + string DataType { get; } + + /// + /// Used for partial validation to ensure that the validator only runs when relevant fields have changed. + /// + /// The current state of the form data + /// The previous state of the form data + bool HasRelevantChanges(object current, object previous); + + /// + /// Returns the group id of the validator. This is used to run partial validations on the backend. + /// The default is based on the FullName and DataType fields, and should not need customization + /// + public string ValidationSource => $"{this.GetType().FullName}-{DataType}"; + + /// + /// The actual validation function + /// + /// + /// + /// + /// List of validation issues + Task> ValidateFormData(Instance instance, DataElement dataElement, object data); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/IInstanceValidator.cs b/src/Altinn.App.Core/Features/IInstanceValidator.cs index 929e65e49..956d0aaeb 100644 --- a/src/Altinn.App.Core/Features/IInstanceValidator.cs +++ b/src/Altinn.App.Core/Features/IInstanceValidator.cs @@ -1,3 +1,4 @@ +using Altinn.App.Core.Features.Validation; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -6,6 +7,7 @@ namespace Altinn.App.Core.Features; /// /// IInstanceValidator defines the methods that are used to validate data and tasks /// +[Obsolete($"Use {nameof(ITaskValidator)}, {nameof(IDataElementValidator)} or {nameof(IFormDataValidator)} instead")] public interface IInstanceValidator { /// diff --git a/src/Altinn.App.Core/Features/ITaskValidator.cs b/src/Altinn.App.Core/Features/ITaskValidator.cs new file mode 100644 index 000000000..d63479c14 --- /dev/null +++ b/src/Altinn.App.Core/Features/ITaskValidator.cs @@ -0,0 +1,41 @@ +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Altinn.App.Core.Features; + +/// +/// Interface for handling validation of tasks. +/// +public interface ITaskValidator +{ + /// + /// The task id this validator is for. Typically either hard coded by implementation or + /// or set by constructor using a and a keyed service. + /// + /// + /// + /// string TaskId { get; init; } + /// // constructor + /// public MyTaskValidator([ServiceKey] string taskId) + /// { + /// TaskId = taskId; + /// } + /// + /// + string TaskId { get; } + + /// + /// Returns the group id of the validator. + /// The default is based on the FullName and TaskId fields, and should not need customization + /// + string ValidationSource => $"{this.GetType().FullName}-{TaskId}"; + + /// + /// Actual validation logic for the task + /// + /// The instance to validate + /// current task to run validations for + /// List of validation issues to add to this task validation + Task> ValidateTask(Instance instance, string taskId); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/DataAnnotationValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/DataAnnotationValidator.cs new file mode 100644 index 000000000..1ec517df2 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/DataAnnotationValidator.cs @@ -0,0 +1,69 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Validation.Helpers; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Runs validation on the data object. +/// +public class DataAnnotationValidator : IFormDataValidator +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IObjectModelValidator _objectModelValidator; + private readonly GeneralSettings _generalSettings; + + /// + /// Constructor + /// + public DataAnnotationValidator(IHttpContextAccessor httpContextAccessor, IObjectModelValidator objectModelValidator, IOptions generalSettings) + { + _httpContextAccessor = httpContextAccessor; + _objectModelValidator = objectModelValidator; + _generalSettings = generalSettings.Value; + } + + /// + /// Run Data annotation validation on all data types with app logic + /// + public string DataType => "*"; + + /// + /// This validator has the code "DataAnnotations" and this is known by the frontend, who may request this validator to not run for incremental validation. + /// + public string ValidationSource => "DataAnnotations"; + + /// + /// We don't know which fields are relevant for data annotation validation, so we always run it. + /// + public bool HasRelevantChanges(object current, object previous) => true; + + /// + public Task> ValidateFormData(Instance instance, DataElement dataElement, object data) + { + try + { + var modelState = new ModelStateDictionary(); + var actionContext = new ActionContext( + _httpContextAccessor.HttpContext!, + new Microsoft.AspNetCore.Routing.RouteData(), + new ActionDescriptor(), + modelState); + ValidationStateDictionary validationState = new ValidationStateDictionary(); + _objectModelValidator.Validate(actionContext, validationState, null!, data); + + return Task.FromResult(ModelStateHelpers.ModelStateToIssueList(modelState, instance, dataElement, _generalSettings, data.GetType(), ValidationIssueSources.ModelState)); + } + catch (Exception e) + { + return Task.FromException>(e); + } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/DefaultDataElementValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/DefaultDataElementValidator.cs new file mode 100644 index 000000000..28a94a041 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/DefaultDataElementValidator.cs @@ -0,0 +1,90 @@ +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Default validations that run on all data elements to validate metadata and file scan results. +/// +public class DefaultDataElementValidator : IDataElementValidator +{ + /// + /// Run validations on all data elements + /// + public string DataType => "*"; + + /// + public Task> ValidateDataElement(Instance instance, DataElement dataElement, DataType dataType) + { + var issues = new List(); + if (dataElement.ContentType == null) + { + issues.Add( new ValidationIssue + { + Code = ValidationIssueCodes.DataElementCodes.MissingContentType, + DataElementId = dataElement.Id, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.DataElementCodes.MissingContentType + }); + } + else + { + var contentTypeWithoutEncoding = dataElement.ContentType.Split(";")[0]; + + if (dataType.AllowedContentTypes != null && dataType.AllowedContentTypes.Count > 0 && + dataType.AllowedContentTypes.TrueForAll(ct => + !ct.Equals(contentTypeWithoutEncoding, StringComparison.OrdinalIgnoreCase))) + { + issues.Add( new ValidationIssue + { + DataElementId = dataElement.Id, + Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, + Field = dataType.Id + }); + } + } + + if (dataType.MaxSize.HasValue && dataType.MaxSize > 0 && + (long)dataType.MaxSize * 1024 * 1024 < dataElement.Size) + { + issues.Add( new ValidationIssue + { + DataElementId = dataElement.Id, + Code = ValidationIssueCodes.DataElementCodes.DataElementTooLarge, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.DataElementCodes.DataElementTooLarge, + Field = dataType.Id + }); + } + + if (dataType.EnableFileScan && dataElement.FileScanResult == FileScanResult.Infected) + { + issues.Add( new ValidationIssue + { + DataElementId = dataElement.Id, + Code = ValidationIssueCodes.DataElementCodes.DataElementFileInfected, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.DataElementCodes.DataElementFileInfected, + Field = dataType.Id + }); + } + + if (dataType.EnableFileScan && dataType.ValidationErrorOnPendingFileScan && + dataElement.FileScanResult == FileScanResult.Pending) + { + issues.Add( new ValidationIssue + { + DataElementId = dataElement.Id, + Code = ValidationIssueCodes.DataElementCodes.DataElementFileScanPending, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.DataElementCodes.DataElementFileScanPending, + Field = dataType.Id + }); + } + + return Task.FromResult(issues); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/DefaultTaskValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/DefaultTaskValidator.cs new file mode 100644 index 000000000..a01b98bd6 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/DefaultTaskValidator.cs @@ -0,0 +1,64 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Enums; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Implement the default validation of DataElements based on the metadata in appMetadata +/// +public class DefaultTaskValidator : ITaskValidator +{ + private readonly IAppMetadata _appMetadata; + + /// + /// Initializes a new instance of the class. + /// + public DefaultTaskValidator(IAppMetadata appMetadata) + { + _appMetadata = appMetadata; + } + + /// + public string TaskId => "*"; + + /// + public async Task> ValidateTask(Instance instance, string taskId) + { + var messages = new List(); + var application = await _appMetadata.GetApplicationMetadata(); + + foreach (var dataType in application.DataTypes.Where(et => et.TaskId == taskId)) + { + List elements = instance.Data.Where(d => d.DataType == dataType.Id).ToList(); + + if (dataType.MaxCount > 0 && dataType.MaxCount < elements.Count) + { + var message = new ValidationIssue + { + Code = ValidationIssueCodes.InstanceCodes.TooManyDataElementsOfType, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.InstanceCodes.TooManyDataElementsOfType, + Field = dataType.Id + }; + messages.Add(message); + } + + if (dataType.MinCount > 0 && dataType.MinCount > elements.Count) + { + var message = new ValidationIssue + { + Code = ValidationIssueCodes.InstanceCodes.TooFewDataElementsOfType, + Severity = ValidationIssueSeverity.Error, + Description = ValidationIssueCodes.InstanceCodes.TooFewDataElementsOfType, + Field = dataType.Id + }; + messages.Add(message); + } + } + + return messages; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs new file mode 100644 index 000000000..df60177c8 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs @@ -0,0 +1,303 @@ +using System.Text.Json; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Validates form data against expression validations +/// +public class ExpressionValidator : IFormDataValidator +{ + private readonly ILogger _logger; + private readonly IAppResources _appResourceService; + private readonly LayoutEvaluatorStateInitializer _layoutEvaluatorStateInitializer; + + private static readonly JsonSerializerOptions _jsonOptions = new() + { + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + /// + /// Constructor for the expression validator + /// + public ExpressionValidator(ILogger logger, IAppResources appResourceService, LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer) + { + _logger = logger; + _appResourceService = appResourceService; + _layoutEvaluatorStateInitializer = layoutEvaluatorStateInitializer; + } + + /// + public string DataType => "*"; + + /// + /// This validator has the code "Expression" and this is known by the frontend, who may request this validator to not run for incremental validation. + /// + public string ValidationSource => "Expression"; + + /// + /// Expression validations should always run (it is way to complex to figure out if it should run or not) + /// + public bool HasRelevantChanges(object current, object previous) => true; + + /// + public async Task> ValidateFormData(Instance instance, DataElement dataElement, object data) + { + var rawValidationConfig = _appResourceService.GetValidationConfiguration(dataElement.DataType); + if (rawValidationConfig == null) + { + // No validation configuration exists for this data type + return new List(); + } + + var validationConfig = JsonDocument.Parse(rawValidationConfig).RootElement; + var evaluatorState = await _layoutEvaluatorStateInitializer.Init(instance, data, dataElement.Id); + return Validate(validationConfig, evaluatorState, _logger); + } + + + internal static List Validate(JsonElement validationConfig, LayoutEvaluatorState evaluatorState, ILogger logger) + { + var validationIssues = new List(); + var expressionValidations = ParseExpressionValidationConfig(validationConfig, logger); + foreach (var validationObject in expressionValidations) + { + var baseField = validationObject.Key; + var resolvedFields = evaluatorState.GetResolvedKeys(baseField); + var validations = validationObject.Value; + foreach (var resolvedField in resolvedFields) + { + var positionalArguments = new[] { resolvedField }; + foreach (var validation in validations) + { + try + { + if (validation.Condition == null) + { + continue; + } + + var isInvalid = ExpressionEvaluator.EvaluateExpression(evaluatorState, validation.Condition, null, positionalArguments); + if (isInvalid is not bool) + { + throw new ArgumentException($"Validation condition for {resolvedField} did not evaluate to a boolean"); + } + if ((bool)isInvalid) + { + var validationIssue = new ValidationIssue + { + Field = resolvedField, + Severity = validation.Severity ?? ValidationIssueSeverity.Error, + CustomTextKey = validation.Message, + Code = validation.Message, + Source = ValidationIssueSources.Expression, + }; + validationIssues.Add(validationIssue); + } + } + catch(Exception e) + { + logger.LogError(e, "Error while evaluating expression validation for {resolvedField}", resolvedField); + throw; + } + } + } + } + + + return validationIssues; + } + + private static RawExpressionValidation? ResolveValidationDefinition(string name, JsonElement definition, Dictionary resolvedDefinitions, ILogger logger) + { + var resolvedDefinition = new RawExpressionValidation(); + + var rawDefinition = definition.Deserialize(_jsonOptions); + if (rawDefinition == null) + { + logger.LogError("Validation definition {name} could not be parsed", name); + return null; + } + if (rawDefinition.Ref != null) + { + var reference = resolvedDefinitions.GetValueOrDefault(rawDefinition.Ref); + if (reference == null) + { + logger.LogError("Could not resolve reference {rawDefinitionRef} for validation {name}", rawDefinition.Ref, name); + return null; + + } + resolvedDefinition.Message = reference.Message; + resolvedDefinition.Condition = reference.Condition; + resolvedDefinition.Severity = reference.Severity; + } + + if (rawDefinition.Message != null) + { + resolvedDefinition.Message = rawDefinition.Message; + } + + if (rawDefinition.Condition != null) + { + resolvedDefinition.Condition = rawDefinition.Condition; + } + + if (rawDefinition.Severity != null) + { + resolvedDefinition.Severity = rawDefinition.Severity; + } + + if (resolvedDefinition.Message == null) + { + logger.LogError("Validation {name} is missing message", name); + return null; + } + + if (resolvedDefinition.Condition == null) + { + logger.LogError("Validation {name} is missing condition", name); + return null; + } + + return resolvedDefinition; + } + + private static ExpressionValidation? ResolveExpressionValidation(string field, JsonElement definition, Dictionary resolvedDefinitions, ILogger logger) + { + + var rawExpressionValidatıon = new RawExpressionValidation(); + + if (definition.ValueKind == JsonValueKind.String) + { + var stringReference = definition.GetString(); + if (stringReference == null) + { + logger.LogError("Could not resolve null reference for validation for field {field}", field); + return null; + } + var reference = resolvedDefinitions.GetValueOrDefault(stringReference); + if (reference == null) + { + logger.LogError("Could not resolve reference {stringReference} for validation for field {field}", stringReference, field); + return null; + } + rawExpressionValidatıon.Message = reference.Message; + rawExpressionValidatıon.Condition = reference.Condition; + rawExpressionValidatıon.Severity = reference.Severity; + } + else + { + var expressionDefinition = definition.Deserialize(_jsonOptions); + if (expressionDefinition == null) + { + logger.LogError("Validation for field {field} could not be parsed", field); + return null; + } + + if (expressionDefinition.Ref != null) + { + var reference = resolvedDefinitions.GetValueOrDefault(expressionDefinition.Ref); + if (reference == null) + { + logger.LogError("Could not resolve reference {expressionDefinitionRef} for validation for field {field}", expressionDefinition.Ref, field); + return null; + + } + rawExpressionValidatıon.Message = reference.Message; + rawExpressionValidatıon.Condition = reference.Condition; + rawExpressionValidatıon.Severity = reference.Severity; + } + + if (expressionDefinition.Message != null) + { + rawExpressionValidatıon.Message = expressionDefinition.Message; + } + + if (expressionDefinition.Condition != null) + { + rawExpressionValidatıon.Condition = expressionDefinition.Condition; + } + + if (expressionDefinition.Severity != null) + { + rawExpressionValidatıon.Severity = expressionDefinition.Severity; + } + } + + if (rawExpressionValidatıon.Message == null) + { + logger.LogError("Validation for field {field} is missing message", field); + return null; + } + + if (rawExpressionValidatıon.Condition == null) + { + logger.LogError("Validation for field {field} is missing condition", field); + return null; + } + + var expressionValidation = new ExpressionValidation + { + Message = rawExpressionValidatıon.Message, + Condition = rawExpressionValidatıon.Condition, + Severity = rawExpressionValidatıon.Severity ?? ValidationIssueSeverity.Error, + }; + + return expressionValidation; + } + + private static Dictionary> ParseExpressionValidationConfig(JsonElement expressionValidationConfig, ILogger logger) + { + var expressionValidationDefinitions = new Dictionary(); + JsonElement definitionsObject; + var hasDefinitions = expressionValidationConfig.TryGetProperty("definitions", out definitionsObject); + if (hasDefinitions) + { + foreach (var definitionObject in definitionsObject.EnumerateObject()) + { + var name = definitionObject.Name; + var definition = definitionObject.Value; + var resolvedDefinition = ResolveValidationDefinition(name, definition, expressionValidationDefinitions, logger); + if (resolvedDefinition == null) + { + logger.LogError("Validation definition {name} could not be resolved", name); + continue; + } + expressionValidationDefinitions[name] = resolvedDefinition; + } + } + var expressionValidations = new Dictionary>(); + JsonElement validationsObject; + var hasValidations = expressionValidationConfig.TryGetProperty("validations", out validationsObject); + if (hasValidations) + { + foreach (var validationArray in validationsObject.EnumerateObject()) + { + var field = validationArray.Name; + var validations = validationArray.Value; + foreach (var validation in validations.EnumerateArray()) + { + if (!expressionValidations.TryGetValue(field, out var expressionValidation)) + { + expressionValidation = new List(); + expressionValidations[field] = expressionValidation; + } + var resolvedExpressionValidation = ResolveExpressionValidation(field, validation, expressionValidationDefinitions, logger); + if (resolvedExpressionValidation == null) + { + logger.LogError("Validation for field {field} could not be resolved", field); + continue; + } + expressionValidation.Add(resolvedExpressionValidation); + } + } + } + return expressionValidations; + } + +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorFormDataValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorFormDataValidator.cs new file mode 100644 index 000000000..51de77bf2 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorFormDataValidator.cs @@ -0,0 +1,51 @@ +#pragma warning disable CS0618 // Type or member is obsolete +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Validation.Helpers; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// This validator is used to run the legacy IInstanceValidator.ValidateData method +/// +public class LegacyIInstanceValidatorFormDataValidator : IFormDataValidator +{ + private readonly IInstanceValidator? _instanceValidator; + private readonly GeneralSettings _generalSettings; + + /// + /// constructor + /// + public LegacyIInstanceValidatorFormDataValidator(IInstanceValidator? instanceValidator, IOptions generalSettings) + { + _instanceValidator = instanceValidator; + _generalSettings = generalSettings.Value; + } + + /// + /// The legacy validator should run for all data types + /// + public string DataType => "*"; + + /// + /// Always run for incremental validation (if it exists) + /// + public bool HasRelevantChanges(object current, object previous) => _instanceValidator is not null; + + + /// + public async Task> ValidateFormData(Instance instance, DataElement dataElement, object data) + { + if (_instanceValidator is null) + { + return new List(); + } + + var modelState = new ModelStateDictionary(); + await _instanceValidator.ValidateData(data, modelState); + return ModelStateHelpers.ModelStateToIssueList(modelState, instance, dataElement, _generalSettings, data.GetType(), ValidationIssueSources.Custom); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs new file mode 100644 index 000000000..9f44fb4c9 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/LegacyIInstanceValidatorTaskValidator.cs @@ -0,0 +1,49 @@ +#pragma warning disable CS0618 // Type or member is obsolete +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Validation.Helpers; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Ensures that the old extention hook is still supported. +/// +public class LegacyIInstanceValidatorTaskValidator : ITaskValidator +{ + private readonly IInstanceValidator? _instanceValidator; + private readonly GeneralSettings _generalSettings; + + /// + /// Constructor + /// + public LegacyIInstanceValidatorTaskValidator(IOptions generalSettings, IInstanceValidator? instanceValidator = null) + { + _instanceValidator = instanceValidator; + _generalSettings = generalSettings.Value; + } + + /// + /// Run the legacy validator for all tasks + /// + public string TaskId => "*"; + + /// + public string ValidationSource => _instanceValidator?.GetType().FullName ?? GetType().FullName!; + + /// + public async Task> ValidateTask(Instance instance, string taskId) + { + if (_instanceValidator is null) + { + return new List(); + } + + var modelState = new ModelStateDictionary(); + await _instanceValidator.ValidateTask(instance, taskId, modelState); + return ModelStateHelpers.MapModelStateToIssueList(modelState, instance, _generalSettings); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Default/RequiredValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/RequiredValidator.cs new file mode 100644 index 000000000..ebb5e39dc --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/RequiredValidator.cs @@ -0,0 +1,52 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Validator that runs the required rules in the layout +/// +public class RequiredLayoutValidator : IFormDataValidator +{ + private readonly LayoutEvaluatorStateInitializer _layoutEvaluatorStateInitializer; + private readonly IAppResources _appResourcesService; + private readonly IAppMetadata _appMetadata; + + /// + /// Initializes a new instance of the class. + /// + public RequiredLayoutValidator(LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, IAppResources appResourcesService, IAppMetadata appMetadata) + { + _layoutEvaluatorStateInitializer = layoutEvaluatorStateInitializer; + _appResourcesService = appResourcesService; + _appMetadata = appMetadata; + } + + /// + /// Run for all data types + /// + public string DataType => "*"; + + /// + /// This validator has the code "Required" and this is known by the frontend, who may request this validator to not run for incremental validation. + /// + public string ValidationSource => "Required"; + + /// + /// Always run for incremental validation + /// + public bool HasRelevantChanges(object current, object previous) => true; + + /// + /// Validate the form data against the required rules in the layout + /// + public async Task> ValidateFormData(Instance instance, DataElement dataElement, object data) + { + var appMetadata = await _appMetadata.GetApplicationMetadata(); + var layoutSet = _appResourcesService.GetLayoutSetForTask(appMetadata.DataTypes.First(dt=>dt.Id == dataElement.DataType).TaskId); + var evaluationState = await _layoutEvaluatorStateInitializer.Init(instance, data, layoutSet?.Id); + return LayoutEvaluator.RunLayoutValidationsForRequired(evaluationState, dataElement.Id); + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/ExpressionValidator.cs b/src/Altinn.App.Core/Features/Validation/ExpressionValidator.cs deleted file mode 100644 index 96329e6ea..000000000 --- a/src/Altinn.App.Core/Features/Validation/ExpressionValidator.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System.Text.Json; -using Altinn.App.Core.Helpers; -using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Internal.Expressions; -using Altinn.App.Core.Models.Validation; -using Microsoft.Extensions.Logging; - - -namespace Altinn.App.Core.Features.Validation -{ - /// - /// Validates form data against expression validations - /// - public static class ExpressionValidator - { - /// - public static IEnumerable Validate(string dataType, IAppResources appResourceService, IDataModelAccessor dataModel, LayoutEvaluatorState evaluatorState, ILogger logger) - { - var rawValidationConfig = appResourceService.GetValidationConfiguration(dataType); - if (rawValidationConfig == null) - { - // No validation configuration exists for this data type - return new List(); - } - - var validationConfig = JsonDocument.Parse(rawValidationConfig).RootElement; - return Validate(validationConfig, dataModel, evaluatorState, logger); - } - - /// - public static IEnumerable Validate(JsonElement validationConfig, IDataModelAccessor dataModel, LayoutEvaluatorState evaluatorState, ILogger logger) - { - var validationIssues = new List(); - var expressionValidations = ParseExpressionValidationConfig(validationConfig, logger); - foreach (var validationObject in expressionValidations) - { - var baseField = validationObject.Key; - var resolvedFields = dataModel.GetResolvedKeys(baseField); - var validations = validationObject.Value; - foreach (var resolvedField in resolvedFields) - { - var positionalArguments = new[] { resolvedField }; - foreach (var validation in validations) - { - try - { - if (validation.Condition == null) - { - continue; - } - - var isInvalid = ExpressionEvaluator.EvaluateExpression(evaluatorState, validation.Condition, null, positionalArguments); - if (isInvalid is not bool) - { - throw new ArgumentException($"Validation condition for {resolvedField} did not evaluate to a boolean"); - } - if ((bool)isInvalid) - { - var validationIssue = new ValidationIssue - { - Field = resolvedField, - Severity = validation.Severity ?? ValidationIssueSeverity.Error, - CustomTextKey = validation.Message, - Code = validation.Message, - Source = ValidationIssueSources.Expression, - }; - validationIssues.Add(validationIssue); - } - } - catch(Exception e) - { - logger.LogError(e, "Error while evaluating expression validation for {resolvedField}", resolvedField); - throw; - } - } - } - } - - - return validationIssues; - } - - private static RawExpressionValidation? ResolveValidationDefinition(string name, JsonElement definition, Dictionary resolvedDefinitions, ILogger logger) - { - var resolvedDefinition = new RawExpressionValidation(); - var rawDefinition = definition.Deserialize(new JsonSerializerOptions - { - ReadCommentHandling = JsonCommentHandling.Skip, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }); - if (rawDefinition == null) - { - logger.LogError($"Validation definition {name} could not be parsed"); - return null; - } - if (rawDefinition.Ref != null) - { - var reference = resolvedDefinitions.GetValueOrDefault(rawDefinition.Ref); - if (reference == null) - { - logger.LogError($"Could not resolve reference {rawDefinition.Ref} for validation {name}"); - return null; - - } - resolvedDefinition.Message = reference.Message; - resolvedDefinition.Condition = reference.Condition; - resolvedDefinition.Severity = reference.Severity; - } - - if (rawDefinition.Message != null) - { - resolvedDefinition.Message = rawDefinition.Message; - } - - if (rawDefinition.Condition != null) - { - resolvedDefinition.Condition = rawDefinition.Condition; - } - - if (rawDefinition.Severity != null) - { - resolvedDefinition.Severity = rawDefinition.Severity; - } - - if (resolvedDefinition.Message == null) - { - logger.LogError($"Validation {name} is missing message"); - return null; - } - - if (resolvedDefinition.Condition == null) - { - logger.LogError($"Validation {name} is missing condition"); - return null; - } - - return resolvedDefinition; - } - - private static ExpressionValidation? ResolveExpressionValidation(string field, JsonElement definition, Dictionary resolvedDefinitions, ILogger logger) - { - - var rawExpressionValidatıon = new RawExpressionValidation(); - - if (definition.ValueKind == JsonValueKind.String) - { - var stringReference = definition.GetString(); - if (stringReference == null) - { - logger.LogError($"Could not resolve null reference for validation for field {field}"); - return null; - } - var reference = resolvedDefinitions.GetValueOrDefault(stringReference); - if (reference == null) - { - logger.LogError($"Could not resolve reference {stringReference} for validation for field {field}"); - return null; - } - rawExpressionValidatıon.Message = reference.Message; - rawExpressionValidatıon.Condition = reference.Condition; - rawExpressionValidatıon.Severity = reference.Severity; - } - else - { - var expressionDefinition = definition.Deserialize(new JsonSerializerOptions - { - ReadCommentHandling = JsonCommentHandling.Skip, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }); - if (expressionDefinition == null) - { - logger.LogError($"Validation for field {field} could not be parsed"); - return null; - } - - if (expressionDefinition.Ref != null) - { - var reference = resolvedDefinitions.GetValueOrDefault(expressionDefinition.Ref); - if (reference == null) - { - logger.LogError($"Could not resolve reference {expressionDefinition.Ref} for validation for field {field}"); - return null; - - } - rawExpressionValidatıon.Message = reference.Message; - rawExpressionValidatıon.Condition = reference.Condition; - rawExpressionValidatıon.Severity = reference.Severity; - } - - if (expressionDefinition.Message != null) - { - rawExpressionValidatıon.Message = expressionDefinition.Message; - } - - if (expressionDefinition.Condition != null) - { - rawExpressionValidatıon.Condition = expressionDefinition.Condition; - } - - if (expressionDefinition.Severity != null) - { - rawExpressionValidatıon.Severity = expressionDefinition.Severity; - } - } - - if (rawExpressionValidatıon.Message == null) - { - logger.LogError($"Validation for field {field} is missing message"); - return null; - } - - if (rawExpressionValidatıon.Condition == null) - { - logger.LogError($"Validation for field {field} is missing condition"); - return null; - } - - var expressionValidation = new ExpressionValidation - { - Message = rawExpressionValidatıon.Message, - Condition = rawExpressionValidatıon.Condition, - Severity = rawExpressionValidatıon.Severity ?? ValidationIssueSeverity.Error, - }; - - return expressionValidation; - } - - private static Dictionary> ParseExpressionValidationConfig(JsonElement expressionValidationConfig, ILogger logger) - { - var expressionValidationDefinitions = new Dictionary(); - JsonElement definitionsObject; - var hasDefinitions = expressionValidationConfig.TryGetProperty("definitions", out definitionsObject); - if (hasDefinitions) - { - foreach (var definitionObject in definitionsObject.EnumerateObject()) - { - var name = definitionObject.Name; - var definition = definitionObject.Value; - var resolvedDefinition = ResolveValidationDefinition(name, definition, expressionValidationDefinitions, logger); - if (resolvedDefinition == null) - { - logger.LogError($"Validation definition {name} could not be resolved"); - continue; - } - expressionValidationDefinitions[name] = resolvedDefinition; - } - } - var expressionValidations = new Dictionary>(); - JsonElement validationsObject; - var hasValidations = expressionValidationConfig.TryGetProperty("validations", out validationsObject); - if (hasValidations) - { - foreach (var validationArray in validationsObject.EnumerateObject()) - { - var field = validationArray.Name; - var validations = validationArray.Value; - foreach (var validation in validations.EnumerateArray()) - { - if (!expressionValidations.ContainsKey(field)) - { - expressionValidations[field] = new List(); - } - var resolvedExpressionValidation = ResolveExpressionValidation(field, validation, expressionValidationDefinitions, logger); - if (resolvedExpressionValidation == null) - { - logger.LogError($"Validation for field {field} could not be resolved"); - continue; - } - expressionValidations[field].Add(resolvedExpressionValidation); - } - } - } - return expressionValidations; - } - } -} diff --git a/src/Altinn.App.Core/Features/Validation/GenericFormDataValidator.cs b/src/Altinn.App.Core/Features/Validation/GenericFormDataValidator.cs new file mode 100644 index 000000000..f09d26585 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/GenericFormDataValidator.cs @@ -0,0 +1,109 @@ +using System.Diagnostics; +using System.Linq.Expressions; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation; + +/// +/// Simple wrapper for validation of form data that does the type checking for you. +/// +/// The type of the model this class will validate +public abstract class GenericFormDataValidator : IFormDataValidator +{ + /// + /// Constructor to force the DataType to be set. + /// + /// The data type this validator should run on + protected GenericFormDataValidator(string dataType) + { + DataType = dataType; + } + /// + public string DataType { get; private init; } + + // ReSharper disable once StaticMemberInGenericType + private static readonly AsyncLocal> ValidationIssues = new(); + + /// + /// Default implementation that respects the runFor prefixes. + /// + public bool HasRelevantChanges(object current, object previous) + { + if (current is not TModel currentCast) + { + throw new Exception($"{GetType().Name} wants to run on data type {DataType}, but the data is of type {current?.GetType().Name}. It should be of type {typeof(TModel).Name}"); + } + + if (previous is not TModel previousCast) + { + throw new Exception($"{GetType().Name} wants to run on data type {DataType}, but the previous of type {previous?.GetType().Name}. It should be of type {typeof(TModel).Name}"); + } + + return HasRelevantChanges(currentCast, previousCast); + } + + + /// + /// Convenience method to create a validation issue for a field using a linq expression instead of a json path for field + /// + /// An expression that is used to attach the issue to a path in the data model + /// The key used to lookup translations for the issue (displayed if lookup fails) + /// The severity for the issue (default Error) + /// Optional description if you want to provide a user friendly message that don't rely on the translation system + /// optional short code for the type of issue + /// List of parameters to replace after looking up the translation. Zero indexed {0} + protected void CreateValidationIssue(Expression> selector, string textKey, ValidationIssueSeverity severity = ValidationIssueSeverity.Error, string? description = null, string? code = null, List? customTextParams = null) + { + Debug.Assert(ValidationIssues.Value is not null); + AddValidationIssue(new ValidationIssue + { + Field = LinqExpressionHelpers.GetJsonPath(selector), + Description = description ?? textKey, + Code = code ?? textKey, + CustomTextKey = textKey, + CustomTextParams = customTextParams, + Severity = severity + }); + } + + /// + /// Allows inheriting classes to add validation issues. + /// + protected void AddValidationIssue(ValidationIssue issue) + { + Debug.Assert(ValidationIssues.Value is not null); + ValidationIssues.Value.Add(issue); + } + + /// + /// Implementation of the generic interface to call the correctly typed + /// validation method implemented by the inheriting class. + /// + public async Task> ValidateFormData(Instance instance, DataElement dataElement, object data) + { + if (data is not TModel model) + { + throw new ArgumentException($"Data is not of type {typeof(TModel)}"); + } + + ValidationIssues.Value = new List(); + await ValidateFormData(instance, dataElement, model); + return ValidationIssues.Value; + + } + + /// + /// Implement this method to validate the data. + /// + protected abstract Task ValidateFormData(Instance instance, DataElement dataElement, TModel data); + + /// + /// Implement this method to check if the data has changed in a way that requires validation. + /// + /// The current data model after applying patches and data processing + /// The previous state before patches and data processing + /// true if the list of validation issues might be different on the two model states + protected abstract bool HasRelevantChanges(TModel current, TModel previous); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/Helpers/ModelStateHelpers.cs b/src/Altinn.App.Core/Features/Validation/Helpers/ModelStateHelpers.cs new file mode 100644 index 000000000..5979663e9 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Helpers/ModelStateHelpers.cs @@ -0,0 +1,174 @@ +using System.Collections; +using System.Text.Json.Serialization; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Altinn.App.Core.Features.Validation.Helpers; + +/// +/// Static helpers to make map from to list of +/// +public static class ModelStateHelpers +{ + /// + /// Get a list of issues from a + /// + /// + /// The instance used for populating issue.InstanceId + /// Data element for populating issue.DataElementId + /// General settings to get *Fixed* prefixes + /// Type of the object to map ModelStateDictionary key to the json path field (might be different) + /// issue.Source + /// A list of the issues as our standard ValidationIssue + public static List ModelStateToIssueList(ModelStateDictionary modelState, Instance instance, + DataElement dataElement, GeneralSettings generalSettings, Type objectType, string source) + { + var validationIssues = new List(); + + foreach (var modelKey in modelState.Keys) + { + modelState.TryGetValue(modelKey, out var entry); + + if (entry is { ValidationState: ModelValidationState.Invalid }) + { + foreach (var error in entry.Errors) + { + var severityAndMessage = GetSeverityFromMessage(error.ErrorMessage, generalSettings); + validationIssues.Add(new ValidationIssue + { + DataElementId = dataElement.Id, + Source = source, + Code = severityAndMessage.Message, + Field = ModelKeyToField(modelKey, objectType)!, + Severity = severityAndMessage.Severity, + Description = severityAndMessage.Message + }); + } + } + } + + return validationIssues; + } + + private static (ValidationIssueSeverity Severity, string Message) GetSeverityFromMessage(string originalMessage, + GeneralSettings generalSettings) + { + if (originalMessage.StartsWith(generalSettings.SoftValidationPrefix)) + { + return (ValidationIssueSeverity.Warning, + originalMessage.Remove(0, generalSettings.SoftValidationPrefix.Length)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + if (originalMessage.StartsWith(generalSettings.FixedValidationPrefix)) + { + return (ValidationIssueSeverity.Fixed, + originalMessage.Remove(0, generalSettings.FixedValidationPrefix.Length)); + } +#pragma warning restore CS0618 // Type or member is obsolete + + if (originalMessage.StartsWith(generalSettings.InfoValidationPrefix)) + { + return (ValidationIssueSeverity.Informational, + originalMessage.Remove(0, generalSettings.InfoValidationPrefix.Length)); + } + + if (originalMessage.StartsWith(generalSettings.SuccessValidationPrefix)) + { + return (ValidationIssueSeverity.Success, + originalMessage.Remove(0, generalSettings.SuccessValidationPrefix.Length)); + } + + return (ValidationIssueSeverity.Error, originalMessage); + } + + /// + /// Translate the ModelKey from validation to a field that respects [JsonPropertyName] annotations + /// + /// + /// Will be obsolete when updating to net70 or higher and activating + /// https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-7.0#use-json-property-names-in-validation-errors + /// + public static string? ModelKeyToField(string? modelKey, Type data) + { + var keyParts = modelKey?.Split('.', 2); + var keyWithIndex = keyParts?.ElementAtOrDefault(0)?.Split('[', 2); + var key = keyWithIndex?.ElementAtOrDefault(0); + var index = keyWithIndex?.ElementAtOrDefault(1); // with traling ']', eg: "3]" + var rest = keyParts?.ElementAtOrDefault(1); + + var properties = data?.GetProperties(); + var property = properties is not null ? Array.Find(properties,p => p.Name == key) : null; + var jsonPropertyName = property + ?.GetCustomAttributes(true) + .OfType() + .FirstOrDefault() + ?.Name; + if (jsonPropertyName is null) + { + jsonPropertyName = key; + } + + if (index is not null) + { + jsonPropertyName = jsonPropertyName + '[' + index; + } + + if (rest is null) + { + return jsonPropertyName; + } + + var childType = property?.PropertyType; + + // Get the Parameter of IEnumerable properties, if they are not string + if (childType is not null && childType != typeof(string) && childType.IsAssignableTo(typeof(IEnumerable))) + { + childType = childType.GetInterfaces() + .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + .Select(t => t.GetGenericArguments()[0]).FirstOrDefault(); + } + + if (childType is null) + { + // Give up and return rest, if the child type is not found. + return $"{jsonPropertyName}.{rest}"; + } + + return $"{jsonPropertyName}.{ModelKeyToField(rest, childType)}"; + } + + /// + /// Same as , but without information about a specific field + /// used by + /// + public static List MapModelStateToIssueList(ModelStateDictionary modelState, Instance instance, + GeneralSettings generalSettings) + { + var validationIssues = new List(); + + foreach (var modelKey in modelState.Keys) + { + modelState.TryGetValue(modelKey, out var entry); + + if (entry != null && entry.ValidationState == ModelValidationState.Invalid) + { + foreach (var error in entry.Errors) + { + var severityAndMessage = GetSeverityFromMessage(error.ErrorMessage, generalSettings); + validationIssues.Add(new ValidationIssue + { + Code = severityAndMessage.Message, + Severity = severityAndMessage.Severity, + Description = severityAndMessage.Message + }); + } + } + } + + return validationIssues; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/IValidation.cs b/src/Altinn.App.Core/Features/Validation/IValidation.cs deleted file mode 100644 index 78c54f791..000000000 --- a/src/Altinn.App.Core/Features/Validation/IValidation.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Altinn.App.Core.Models.Validation; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Features.Validation -{ - /// - /// Describes the public methods of a validation service - /// - public interface IValidation - { - /// - /// Validate an instance for a specified process step. - /// - /// The instance to validate - /// The task to validate the instance for. - /// A list of validation errors if any were found - Task> ValidateAndUpdateProcess(Instance instance, string taskId); - - /// - /// Validate a specific data element. - /// - /// The instance where the data element belong - /// The datatype describing the data element requirements - /// The metadata of a data element to validate - /// A list of validation errors if any were found - Task> ValidateDataElement(Instance instance, DataType dataType, DataElement dataElement); - } -} diff --git a/src/Altinn.App.Core/Features/Validation/IValidationService.cs b/src/Altinn.App.Core/Features/Validation/IValidationService.cs new file mode 100644 index 000000000..1a345b0b7 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/IValidationService.cs @@ -0,0 +1,54 @@ +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; + +namespace Altinn.App.Core.Features.Validation; + +/// +/// Core interface for validation of instances. Only a single implementation of this interface should exist in the app. +/// +public interface IValidationService +{ + /// + /// Validates the instance with all data elements on the current task and ensures that the instance is ready for process next. + /// + /// + /// This method executes validations in the following interfaces + /// * for the current task + /// * for all data elements on the current task + /// * for all data elements with app logic on the current task + /// + /// The instance to validate + /// instance.Process?.CurrentTask?.ElementId + /// List of validation issues for this data element + Task> ValidateInstanceAtTask(Instance instance, string taskId); + + /// + /// + /// + /// + /// This method executes validations in the following interfaces + /// * for all data elements on the current task + /// * for all data elements with app logic on the current task + /// + /// This method does not run task validations + /// + /// The instance to validate + /// The data element to run validations for + /// The data type (from applicationmetadata) that the element is an instance of + /// List of validation issues for this data element + Task> ValidateDataElement(Instance instance, DataElement dataElement, DataType dataType); + + /// + /// Validates a single data element. Used by frontend to continuously validate form data as it changes. + /// + /// + /// This method executes validations for + /// + /// The instance to validate + /// The data element to run validations for + /// The type of the data element + /// The data deserialized to the strongly typed object that represents the form data + /// The previous data so that validators can know if they need to run again with + /// List validators that should not be run (for incremental validation). Typically known validators that frontend knows how to replicate + Task>> ValidateFormData(Instance instance, DataElement dataElement, DataType dataType, object data, object? previousData = null, List? ignoredValidators = null); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/NullInstanceValidator.cs b/src/Altinn.App.Core/Features/Validation/NullInstanceValidator.cs deleted file mode 100644 index 4e3919be4..000000000 --- a/src/Altinn.App.Core/Features/Validation/NullInstanceValidator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Altinn.Platform.Storage.Interface.Models; -using Microsoft.AspNetCore.Mvc.ModelBinding; - -namespace Altinn.App.Core.Features.Validation; - -/// -/// Default implementation of the IInstanceValidator interface. -/// This implementation does not do any validation and always returns true. -/// -public class NullInstanceValidator: IInstanceValidator -{ - /// - public async Task ValidateData(object data, ModelStateDictionary validationResults) - { - await Task.CompletedTask; - } - - /// - public async Task ValidateTask(Instance instance, string taskId, ModelStateDictionary validationResults) - { - await Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs b/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs deleted file mode 100644 index 5379c6f0c..000000000 --- a/src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs +++ /dev/null @@ -1,433 +0,0 @@ -using Altinn.App.Core.Configuration; -using Altinn.App.Core.Helpers.DataModel; -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.Models.Validation; -using Altinn.Platform.Storage.Interface.Enums; -using Altinn.Platform.Storage.Interface.Models; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Altinn.App.Core.Features.Validation -{ - /// - /// Represents a validation service for validating instances and their data elements - /// - public class ValidationAppSI : IValidation - { - private readonly ILogger _logger; - private readonly IDataClient _dataClient; - private readonly IInstanceClient _instanceClient; - private readonly IInstanceValidator _instanceValidator; - private readonly IAppModel _appModel; - private readonly IAppResources _appResourcesService; - private readonly IAppMetadata _appMetadata; - private readonly LayoutEvaluatorStateInitializer _layoutEvaluatorStateInitializer; - private readonly IObjectModelValidator _objectModelValidator; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly GeneralSettings _generalSettings; - private readonly AppSettings _appSettings; - - /// - /// Initializes a new instance of the class. - /// - public ValidationAppSI( - ILogger logger, - IDataClient dataClient, - IInstanceClient instanceClient, - IInstanceValidator instanceValidator, - IAppModel appModel, - IAppResources appResourcesService, - IAppMetadata appMetadata, - IObjectModelValidator objectModelValidator, - LayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, - IHttpContextAccessor httpContextAccessor, - IOptions generalSettings, - IOptions appSettings) - { - _logger = logger; - _dataClient = dataClient; - _instanceClient = instanceClient; - _instanceValidator = instanceValidator; - _appModel = appModel; - _appResourcesService = appResourcesService; - _appMetadata = appMetadata; - _objectModelValidator = objectModelValidator; - _layoutEvaluatorStateInitializer = layoutEvaluatorStateInitializer; - _httpContextAccessor = httpContextAccessor; - _generalSettings = generalSettings.Value; - _appSettings = appSettings.Value; - } - - /// - /// Validate an instance for a specified process step. - /// - /// The instance to validate - /// The task to validate the instance for. - /// A list of validation errors if any were found - public async Task> ValidateAndUpdateProcess(Instance instance, string taskId) - { - _logger.LogInformation("Validation of {instance.Id}", instance.Id); - - List messages = new List(); - - ModelStateDictionary validationResults = new ModelStateDictionary(); - await _instanceValidator.ValidateTask(instance, taskId, validationResults); - messages.AddRange(MapModelStateToIssueList(validationResults, instance)); - - Application application = await _appMetadata.GetApplicationMetadata(); - - foreach (DataType dataType in application.DataTypes.Where(et => et.TaskId == taskId)) - { - List elements = instance.Data.Where(d => d.DataType == dataType.Id).ToList(); - - if (dataType.MaxCount > 0 && dataType.MaxCount < elements.Count) - { - ValidationIssue message = new ValidationIssue - { - InstanceId = instance.Id, - Code = ValidationIssueCodes.InstanceCodes.TooManyDataElementsOfType, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.InstanceCodes.TooManyDataElementsOfType, - Field = dataType.Id - }; - messages.Add(message); - } - - if (dataType.MinCount > 0 && dataType.MinCount > elements.Count) - { - ValidationIssue message = new ValidationIssue - { - InstanceId = instance.Id, - Code = ValidationIssueCodes.InstanceCodes.TooFewDataElementsOfType, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.InstanceCodes.TooFewDataElementsOfType, - Field = dataType.Id - }; - messages.Add(message); - } - - foreach (DataElement dataElement in elements) - { - messages.AddRange(await ValidateDataElement(instance, dataType, dataElement)); - } - } - - instance.Process.CurrentTask.Validated = new ValidationStatus - { - // The condition for completion is met if there are no errors (or other weirdnesses). - CanCompleteTask = messages.Count == 0 || - messages.All(m => m.Severity != ValidationIssueSeverity.Error && m.Severity != ValidationIssueSeverity.Unspecified), - Timestamp = DateTime.UtcNow - }; - - await _instanceClient.UpdateProcess(instance); - return messages; - } - - /// - /// Validate a specific data element. - /// - /// The instance where the data element belong - /// The datatype describing the data element requirements - /// The metadata of a data element to validate - /// A list of validation errors if any were found - public async Task> ValidateDataElement(Instance instance, DataType dataType, DataElement dataElement) - { - _logger.LogInformation("Validation of data element {dataElement.Id} of instance {instance.Id}", dataElement.Id, instance.Id); - - List messages = new List(); - - if (dataElement.ContentType == null) - { - ValidationIssue message = new ValidationIssue - { - InstanceId = instance.Id, - Code = ValidationIssueCodes.DataElementCodes.MissingContentType, - DataElementId = dataElement.Id, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.DataElementCodes.MissingContentType - }; - messages.Add(message); - } - else - { - string contentTypeWithoutEncoding = dataElement.ContentType.Split(";")[0]; - - if (dataType.AllowedContentTypes != null && dataType.AllowedContentTypes.Count > 0 && dataType.AllowedContentTypes.All(ct => !ct.Equals(contentTypeWithoutEncoding, StringComparison.OrdinalIgnoreCase))) - { - ValidationIssue message = new ValidationIssue - { - InstanceId = instance.Id, - DataElementId = dataElement.Id, - Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, - Field = dataType.Id - }; - messages.Add(message); - } - } - - if (dataType.MaxSize.HasValue && dataType.MaxSize > 0 && (long)dataType.MaxSize * 1024 * 1024 < dataElement.Size) - { - ValidationIssue message = new ValidationIssue - { - InstanceId = instance.Id, - DataElementId = dataElement.Id, - Code = ValidationIssueCodes.DataElementCodes.DataElementTooLarge, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.DataElementCodes.DataElementTooLarge, - Field = dataType.Id - }; - messages.Add(message); - } - - if (dataType.EnableFileScan && dataElement.FileScanResult == FileScanResult.Infected) - { - ValidationIssue message = new ValidationIssue() - { - InstanceId = instance.Id, - DataElementId = dataElement.Id, - Code = ValidationIssueCodes.DataElementCodes.DataElementFileInfected, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.DataElementCodes.DataElementFileInfected, - Field = dataType.Id - }; - messages.Add(message); - } - - if (dataType.EnableFileScan && dataType.ValidationErrorOnPendingFileScan && dataElement.FileScanResult == FileScanResult.Pending) - { - ValidationIssue message = new ValidationIssue() - { - InstanceId = instance.Id, - DataElementId = dataElement.Id, - Code = ValidationIssueCodes.DataElementCodes.DataElementFileScanPending, - Severity = ValidationIssueSeverity.Error, - Description = ValidationIssueCodes.DataElementCodes.DataElementFileScanPending, - Field = dataType.Id - }; - messages.Add(message); - } - - if (dataType.AppLogic?.ClassRef != null) - { - Type modelType = _appModel.GetModelType(dataType.AppLogic.ClassRef); - Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); - string app = instance.AppId.Split("/")[1]; - int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); - object data = await _dataClient.GetFormData( - instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, Guid.Parse(dataElement.Id)); - - LayoutEvaluatorState? evaluationState = null; - - // Remove hidden data before validation - if (_appSettings.RequiredValidation || _appSettings.ExpressionValidation) - { - - var layoutSet = _appResourcesService.GetLayoutSetForTask(dataType.TaskId); - evaluationState = await _layoutEvaluatorStateInitializer.Init(instance, data, layoutSet?.Id); - LayoutEvaluator.RemoveHiddenData(evaluationState, RowRemovalOption.SetToNull); - } - - // Evaluate expressions in layout and validate that all required data is included and that maxLength - // is respected on groups - if (_appSettings.RequiredValidation) - { - var layoutErrors = LayoutEvaluator.RunLayoutValidationsForRequired(evaluationState!, dataElement.Id); - messages.AddRange(layoutErrors); - } - - // Run expression validations - if (_appSettings.ExpressionValidation) - { - var expressionErrors = ExpressionValidator.Validate(dataType.Id, _appResourcesService, new DataModel(data), evaluationState!, _logger); - messages.AddRange(expressionErrors); - } - - // Run Standard mvc validation using the System.ComponentModel.DataAnnotations - ModelStateDictionary dataModelValidationResults = new ModelStateDictionary(); - var actionContext = new ActionContext( - _httpContextAccessor.HttpContext, - new Microsoft.AspNetCore.Routing.RouteData(), - new ActionDescriptor(), - dataModelValidationResults); - ValidationStateDictionary validationState = new ValidationStateDictionary(); - _objectModelValidator.Validate(actionContext, validationState, null, data); - - if (!dataModelValidationResults.IsValid) - { - messages.AddRange(MapModelStateToIssueList(actionContext.ModelState, ValidationIssueSources.ModelState, instance, dataElement.Id, data.GetType())); - } - - // Call custom validation from the IInstanceValidator - ModelStateDictionary customValidationResults = new ModelStateDictionary(); - await _instanceValidator.ValidateData(data, customValidationResults); - - if (!customValidationResults.IsValid) - { - messages.AddRange(MapModelStateToIssueList(customValidationResults, ValidationIssueSources.Custom, instance, dataElement.Id, data.GetType())); - } - - } - - return messages; - } - - private List MapModelStateToIssueList( - ModelStateDictionary modelState, - string source, - Instance instance, - string dataElementId, - Type modelType) - { - List validationIssues = new List(); - - foreach (string modelKey in modelState.Keys) - { - modelState.TryGetValue(modelKey, out ModelStateEntry? entry); - - if (entry != null && entry.ValidationState == ModelValidationState.Invalid) - { - foreach (ModelError error in entry.Errors) - { - var severityAndMessage = GetSeverityFromMessage(error.ErrorMessage); - validationIssues.Add(new ValidationIssue - { - InstanceId = instance.Id, - DataElementId = dataElementId, - Source = source, - Code = severityAndMessage.Message, - Field = ModelKeyToField(modelKey, modelType)!, - Severity = severityAndMessage.Severity, - Description = severityAndMessage.Message - }); - } - } - } - - return validationIssues; - } - - /// - /// Translate the ModelKey from validation to a field that respects [JsonPropertyName] annotations - /// - /// - /// Will be obsolete when updating to net70 or higher and activating https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-7.0#use-json-property-names-in-validation-errors - /// - public static string? ModelKeyToField(string? modelKey, Type data) - { - var keyParts = modelKey?.Split('.', 2); - var keyWithIndex = keyParts?.ElementAtOrDefault(0)?.Split('[', 2); - var key = keyWithIndex?.ElementAtOrDefault(0); - var index = keyWithIndex?.ElementAtOrDefault(1); // with traling ']', eg: "3]" - var rest = keyParts?.ElementAtOrDefault(1); - - var property = data?.GetProperties()?.FirstOrDefault(p => p.Name == key); - var jsonPropertyName = property - ?.GetCustomAttributes(true) - .OfType() - .FirstOrDefault() - ?.Name; - if (jsonPropertyName is null) - { - jsonPropertyName = key; - } - - if (index is not null) - { - jsonPropertyName = jsonPropertyName + '[' + index; - } - - if (rest is null) - { - return jsonPropertyName; - } - - var childType = property?.PropertyType; - - // Get the Parameter of IEnumerable properties, if they are not string - if (childType is not null && childType != typeof(string) && childType.IsAssignableTo(typeof(System.Collections.IEnumerable))) - { - childType = childType.GetInterfaces() - .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - .Select(t => t.GetGenericArguments()[0]).FirstOrDefault(); - } - - if (childType is null) - { - // Give up and return rest, if the child type is not found. - return $"{jsonPropertyName}.{rest}"; - } - - return $"{jsonPropertyName}.{ModelKeyToField(rest, childType)}"; - } - - private List MapModelStateToIssueList(ModelStateDictionary modelState, Instance instance) - { - List validationIssues = new List(); - - foreach (string modelKey in modelState.Keys) - { - modelState.TryGetValue(modelKey, out ModelStateEntry? entry); - - if (entry != null && entry.ValidationState == ModelValidationState.Invalid) - { - foreach (ModelError error in entry.Errors) - { - var severityAndMessage = GetSeverityFromMessage(error.ErrorMessage); - validationIssues.Add(new ValidationIssue - { - InstanceId = instance.Id, - Code = severityAndMessage.Message, - Severity = severityAndMessage.Severity, - Description = severityAndMessage.Message - }); - } - } - } - - return validationIssues; - } - - private (ValidationIssueSeverity Severity, string Message) GetSeverityFromMessage(string originalMessage) - { - if (originalMessage.StartsWith(_generalSettings.SoftValidationPrefix)) - { - return (ValidationIssueSeverity.Warning, - originalMessage.Remove(0, _generalSettings.SoftValidationPrefix.Length)); - } - - if (_generalSettings.FixedValidationPrefix != null - && originalMessage.StartsWith(_generalSettings.FixedValidationPrefix)) - { - return (ValidationIssueSeverity.Fixed, - originalMessage.Remove(0, _generalSettings.FixedValidationPrefix.Length)); - } - - if (originalMessage.StartsWith(_generalSettings.InfoValidationPrefix)) - { - return (ValidationIssueSeverity.Informational, - originalMessage.Remove(0, _generalSettings.InfoValidationPrefix.Length)); - } - - if (originalMessage.StartsWith(_generalSettings.SuccessValidationPrefix)) - { - return (ValidationIssueSeverity.Success, - originalMessage.Remove(0, _generalSettings.SuccessValidationPrefix.Length)); - } - - return (ValidationIssueSeverity.Error, originalMessage); - } - } -} diff --git a/src/Altinn.App.Core/Features/Validation/ValidationService.cs b/src/Altinn.App.Core/Features/Validation/ValidationService.cs new file mode 100644 index 000000000..4dbff6bca --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/ValidationService.cs @@ -0,0 +1,156 @@ +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Features.Validation; + +/// +/// Main validation service that encapsulates all validation logic +/// +public class ValidationService : IValidationService +{ + private readonly IServiceProvider _serviceProvider; + private readonly IDataClient _dataClient; + private readonly IAppModel _appModel; + private readonly IAppMetadata _appMetadata; + private readonly ILogger _logger; + + /// + /// Constructor with DI services + /// + public ValidationService(IServiceProvider serviceProvider, IDataClient dataClient, IAppModel appModel, IAppMetadata appMetadata, ILogger logger) + { + _serviceProvider = serviceProvider; + _dataClient = dataClient; + _appModel = appModel; + _appMetadata = appMetadata; + _logger = logger; + } + + /// + public async Task> ValidateInstanceAtTask(Instance instance, string taskId) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentNullException.ThrowIfNull(taskId); + + // Run task validations + var taskValidators = _serviceProvider.GetServices() + .Where(tv => tv.TaskId == "*" || tv.TaskId == taskId) + // .Concat(_serviceProvider.GetKeyedServices(taskId)) + .ToArray(); + + var taskIssuesTask = Task.WhenAll(taskValidators.Select(async tv => + { + try + { + _logger.LogDebug("Start running validator {validatorName} on task {taskId} in instance {instanceId}", tv.GetType().Name, taskId, instance.Id); + var issues = await tv.ValidateTask(instance, taskId); + issues.ForEach(i => i.Source = tv.ValidationSource); // Ensure that the source is set to the validator source + return issues; + } + catch (Exception e) + { + _logger.LogError(e, "Error while running validator {validatorName} on task {taskId} in instance {instanceId}", tv.GetType().Name, taskId, instance.Id); + throw; + } + })); + + // Run validations for single data elements + var application = await _appMetadata.GetApplicationMetadata(); + var dataTypesForTask = application.DataTypes.Where(dt => dt.TaskId == taskId).ToList(); + var dataElementsToValidate = instance.Data.Where(de => dataTypesForTask.Exists(dt => dt.Id == de.DataType)).ToArray(); + var dataIssuesTask = Task.WhenAll(dataElementsToValidate.Select(dataElement=>ValidateDataElement(instance, dataElement, dataTypesForTask.First(dt=>dt.Id == dataElement.DataType) ))); + + return (await Task.WhenAll(taskIssuesTask, dataIssuesTask)).SelectMany(x=>x.SelectMany(y=>y)).ToList(); + } + + + /// + public async Task> ValidateDataElement(Instance instance, DataElement dataElement, DataType dataType) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentNullException.ThrowIfNull(dataElement); + ArgumentNullException.ThrowIfNull(dataElement.DataType); + + // Get both keyed and non-keyed validators for the data type + var validators = _serviceProvider.GetServices() + .Where(v => v.DataType == "*" || v.DataType == dataType.Id) + // .Concat(_serviceProvider.GetKeyedServices(dataElement.DataType)) + .ToArray(); + + var dataElementsIssuesTask = Task.WhenAll(validators.Select(async v => + { + try + { + _logger.LogDebug("Start running validator {validatorName} on {dataType} for data element {dataElementId} in instance {instanceId}", v.GetType().Name, dataElement.DataType, dataElement.Id, instance.Id); + var issues = await v.ValidateDataElement(instance, dataElement, dataType); + issues.ForEach(i => i.Source = v.ValidationSource); // Ensure that the source is set to the validator source + return issues; + } + catch (Exception e) + { + _logger.LogError(e, "Error while running validator {validatorName} on {dataType} for data element {dataElementId} in instance {instanceId}", v.GetType().Name, dataElement.DataType, dataElement.Id, instance.Id); + throw; + } + })); + + // Run extra validation on form data elements with app logic + if(dataType.AppLogic?.ClassRef is not null) + { + Type modelType = _appModel.GetModelType(dataType.AppLogic.ClassRef); + + Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + string app = instance.AppId.Split("/")[1]; + int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); + var data = await _dataClient.GetFormData(instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, Guid.Parse(dataElement.Id)); // TODO: Add method that accepts instance and dataElement + var formDataIssuesDictionary = await ValidateFormData(instance, dataElement, dataType, data); + + return (await dataElementsIssuesTask).SelectMany(x=>x) + .Concat(formDataIssuesDictionary.SelectMany(kv=>kv.Value)) + .ToList(); + } + + return (await dataElementsIssuesTask).SelectMany(x=>x).ToList(); + } + + /// + public async Task>> ValidateFormData(Instance instance, DataElement dataElement, DataType dataType, object data, + object? previousData = null, List? ignoredValidators = null) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentNullException.ThrowIfNull(dataElement); + ArgumentNullException.ThrowIfNull(dataElement.DataType); + ArgumentNullException.ThrowIfNull(data); + + // Locate the relevant data validator services from normal and keyed services + var dataValidators = _serviceProvider.GetServices() + .Where(dv => dv.DataType == "*" || dv.DataType == dataType.Id) + // .Concat(_serviceProvider.GetKeyedServices(dataElement.DataType)) + .Where(dv => ignoredValidators?.Contains(dv.ValidationSource) != true) + .Where(dv => previousData is null || dv.HasRelevantChanges(data, previousData)) + .ToArray(); + + var issuesLists = await Task.WhenAll(dataValidators.Select(async (v) => + { + try + { + _logger.LogDebug("Start running validator {validatorName} on {dataType} for data element {dataElementId} in instance {instanceId}", v.GetType().Name, dataElement.DataType, dataElement.Id, instance.Id); + var issues = await v.ValidateFormData(instance, dataElement, data); + issues.ForEach(i => i.Source = v.ValidationSource);// Ensure that the code is set to the validator code + return issues; + } + catch (Exception e) + { + _logger.LogError(e, "Error while running validator {validatorName} on {dataType} for data element {dataElementId} in instance {instanceId}", v.GetType().Name, dataElement.DataType, dataElement.Id, instance.Id); + throw; + } + })); + + return dataValidators.Zip(issuesLists).ToDictionary(kv => kv.First.ValidationSource, kv => kv.Second); + } + +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Helpers/JsonHelper.cs b/src/Altinn.App.Core/Helpers/JsonHelper.cs index ee8bb456f..538312165 100644 --- a/src/Altinn.App.Core/Helpers/JsonHelper.cs +++ b/src/Altinn.App.Core/Helpers/JsonHelper.cs @@ -16,7 +16,7 @@ public static class JsonHelper /// /// Run DataProcessWrite returning the dictionary of the changed fields. /// - public static async Task?> ProcessDataWriteWithDiff(Instance instance, Guid dataGuid, object serviceModel, IEnumerable dataProcessors, Dictionary? changedFields, ILogger logger) + public static async Task?> ProcessDataWriteWithDiff(Instance instance, Guid dataGuid, object serviceModel, IEnumerable dataProcessors, ILogger logger) { if (!dataProcessors.Any()) { @@ -27,7 +27,7 @@ public static class JsonHelper foreach (var dataProcessor in dataProcessors) { logger.LogInformation("ProcessDataRead for {modelType} using {dataProcesor}", serviceModel.GetType().Name, dataProcessor.GetType().Name); - await dataProcessor.ProcessDataWrite(instance, dataGuid, serviceModel, changedFields); + await dataProcessor.ProcessDataWrite(instance, dataGuid, serviceModel, null); } string updatedServiceModelString = System.Text.Json.JsonSerializer.Serialize(serviceModel); diff --git a/src/Altinn.App.Core/Helpers/LinqExpressionHelpers.cs b/src/Altinn.App.Core/Helpers/LinqExpressionHelpers.cs new file mode 100644 index 000000000..406f815f4 --- /dev/null +++ b/src/Altinn.App.Core/Helpers/LinqExpressionHelpers.cs @@ -0,0 +1,83 @@ +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json.Serialization; + +namespace Altinn.App.Core.Helpers; + +/// +/// Utilities for working with +/// +public static class LinqExpressionHelpers +{ + /// + /// Gets the JSON path from an expression + /// + /// The expression + /// The JSON path + public static string GetJsonPath(Expression> expression) + { + return GetJsonPath_internal(expression); + } + + /// + /// Need a private method to avoid the generic type parameter for recursion + /// + private static string GetJsonPath_internal(Expression expression) + { + ArgumentNullException.ThrowIfNull(expression); + + var path = new List(); + Expression? current = expression; + while (current is not null) + { + switch (current) + { + case MemberExpression memberExpression: + path.Add(GetJsonPropertyName(memberExpression.Member)); + current = memberExpression.Expression; + break; + case LambdaExpression lambdaExpression: + current = lambdaExpression.Body; + break; + case ParameterExpression: + // We have reached the root of the expression + current = null; + break; + + // This is a special case for accessing a list item by index + case MethodCallExpression { Method.Name: "get_Item", Arguments: [ ConstantExpression { Value: Int32 index } ], Object: MemberExpression memberExpression }: + path.Add($"{GetJsonPropertyName(memberExpression.Member)}[{index}]"); + current = memberExpression.Expression; + break; + // This is a special case for accessing a list item by index in a variable + case MethodCallExpression { Method.Name: "get_Item", Arguments: [ MemberExpression { Expression: ConstantExpression constantExpression, Member: FieldInfo fieldInfo }], Object: MemberExpression memberExpression }: + // Evaluate the constant expression to get the index + var evaluatedIndex = fieldInfo.GetValue(constantExpression.Value); + path.Add($"{GetJsonPropertyName(memberExpression.Member)}[{evaluatedIndex}]"); + current = memberExpression.Expression; + break; + // This is a special case for selecting all childern of a list using Select + case MethodCallExpression { Method.Name: "Select" } methodCallExpression: + path.Add(GetJsonPath_internal(methodCallExpression.Arguments[1])); + current = methodCallExpression.Arguments[0]; + break; + default: + throw new ArgumentException($"Invalid expression {expression}. Failed reading {current}"); + } + } + + path.Reverse(); + return string.Join(".", path); + } + + private static string GetJsonPropertyName(MemberInfo memberExpressionMember) + { + var jsonPropertyAttribute = memberExpressionMember.GetCustomAttribute(); + if (jsonPropertyAttribute is not null) + { + return jsonPropertyAttribute.Name; + } + + return memberExpressionMember.Name; + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Helpers/ObjectUtils.cs b/src/Altinn.App.Core/Helpers/ObjectUtils.cs new file mode 100644 index 000000000..30f363af7 --- /dev/null +++ b/src/Altinn.App.Core/Helpers/ObjectUtils.cs @@ -0,0 +1,54 @@ +using System.Collections; + +namespace Altinn.App.Core.Helpers; + +/// +/// Utilities for working with model instances +/// +public static class ObjectUtils +{ + /// + /// Recursively initialize all properties on the object that are currently null + /// Also ensure that all string properties that are empty are set to null + /// + /// The object to mutate + public static void InitializeListsAndNullEmptyStrings(object model) + { + foreach (var prop in model.GetType().GetProperties()) + { + if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) + { + var value = prop.GetValue(model); + if (value is null) + { + // Initialize IList with null value + prop.SetValue(model, Activator.CreateInstance(prop.PropertyType)); + } + else + { + foreach (var item in (IList)value) + { + // Recurse into values of a list + InitializeListsAndNullEmptyStrings(item); + } + } + } + else if (prop.GetIndexParameters().Length == 0) + { + var value = prop.GetValue(model); + + if (value is "") + { + // Initialize string with null value (xml serialization does not always preserve "") + prop.SetValue(model, null); + } + + // continue recursion over all properties + if (value is not null) + { + InitializeListsAndNullEmptyStrings(value); + } + } + } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs index ae76b600c..7ee5921ac 100644 --- a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs +++ b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializer.cs @@ -1,9 +1,10 @@ - +using System; +using System.IO; using System.Text; +using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Net.Http.Headers; + using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -18,6 +19,11 @@ public class ModelDeserializer private readonly ILogger _logger; private readonly Type _modelType; + /// + /// Gets the error message describing what it was that went wrong if there was an issue during deserialization. + /// + public string? Error { get; private set; } + /// /// Initialize a new instance of with a logger and the Type the deserializer should target. /// @@ -35,17 +41,14 @@ public ModelDeserializer(ILogger logger, Type modelType) /// The data stream to deserialize. /// The content type of the stream. /// An instance of the initialized type if deserializing succeed. - public async Task DeserializeAsync(Stream stream, string? contentType) + public async Task DeserializeAsync(Stream stream, string? contentType) { + Error = null; if (contentType == null) { - return ModelDeserializerResult.FromError($"Unknown content type \"null\". Cannot read the data."); - } - - if (contentType.Contains("multipart/form-data")) - { - return await DeserializeMultipartAsync(stream, contentType); + Error = $"Unknown content type \"null\". Cannot read the data."; + return null; } if (contentType.Contains("application/json")) @@ -58,74 +61,50 @@ public async Task DeserializeAsync(Stream stream, strin return await DeserializeXmlAsync(stream); } - return ModelDeserializerResult.FromError($"Unknown content type {contentType}. Cannot read the data."); + Error = $"Unknown content type {contentType}. Cannot read the data."; + return null; } - private async Task DeserializeMultipartAsync(Stream stream, string contentType) + private async Task DeserializeJsonAsync(Stream stream) { - MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(contentType); - string boundary = mediaType.Boundary.Value!.Trim('"'); - var reader = new MultipartReader(boundary, stream); - FormMultipartSection? firstSection = (await reader.ReadNextSectionAsync())?.AsFormDataSection(); - if (firstSection?.Name != "dataModel") - { - return ModelDeserializerResult.FromError("First entry in multipart serialization must have name=\"dataModel\""); - } - var modelResult = await DeserializeJsonAsync(firstSection.Section.Body); - if (modelResult.HasError) - { - return modelResult; - } - - FormMultipartSection? secondSection = (await reader.ReadNextSectionAsync())?.AsFormDataSection(); - Dictionary? reportedChanges = null; - if (secondSection is not null) - { - if (secondSection.Name != "previousValues") - { - return ModelDeserializerResult.FromError("Second entry in multipart serialization must have name=\"previousValues\""); - } - reportedChanges = await System.Text.Json.JsonSerializer.DeserializeAsync>(secondSection.Section.Body); - if (await reader.ReadNextSectionAsync() != null) - { - return ModelDeserializerResult.FromError("Multipart request had more than 2 elements. Only \"dataModel\" and the optional \"previousValues\" are supported."); - } - } - return ModelDeserializerResult.FromSuccess(modelResult.Model, reportedChanges); - } + Error = null; - private async Task DeserializeJsonAsync(Stream stream) - { try { using StreamReader reader = new StreamReader(stream, Encoding.UTF8); string content = await reader.ReadToEndAsync(); - return ModelDeserializerResult.FromSuccess(JsonConvert.DeserializeObject(content, _modelType)); + return JsonConvert.DeserializeObject(content, _modelType)!; } catch (JsonReaderException jsonReaderException) { - return ModelDeserializerResult.FromError(jsonReaderException.Message); + Error = jsonReaderException.Message; + return null; } catch (Exception ex) { - _logger.LogError(ex, "Unexpected exception when attempting to deserialize JSON into '{modelType}'", _modelType); - return ModelDeserializerResult.FromError($"Unexpected exception when attempting to deserialize JSON into '{_modelType}'"); + string message = $"Unexpected exception when attempting to deserialize JSON into '{_modelType}'"; + _logger.LogError(ex, message); + Error = message; + return null; } - } - private async Task DeserializeXmlAsync(Stream stream) + private async Task DeserializeXmlAsync(Stream stream) { - // In this first try block we assume that the namespace is the same in the model - // and in the XML. This includes no namespace in both. - using StreamReader reader = new StreamReader(stream, Encoding.UTF8); - string? streamContent = await reader.ReadToEndAsync(); + Error = null; + + string streamContent = null; try { + // In this first try block we assume that the namespace is the same in the model + // and in the XML. This includes no namespace in both. + using StreamReader reader = new StreamReader(stream, Encoding.UTF8); + streamContent = await reader.ReadToEndAsync(); + using XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(streamContent)); XmlSerializer serializer = new XmlSerializer(_modelType); - return ModelDeserializerResult.FromSuccess(serializer.Deserialize(xmlTextReader)); + return serializer.Deserialize(xmlTextReader); } catch (InvalidOperationException) { @@ -143,18 +122,21 @@ private async Task DeserializeXmlAsync(Stream stream) using XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(streamContent)); XmlSerializer serializer = new XmlSerializer(_modelType, attributeOverrides); - return ModelDeserializerResult.FromSuccess(serializer.Deserialize(xmlTextReader)); + return serializer.Deserialize(xmlTextReader); } catch (InvalidOperationException invalidOperationException) { // One possible fail condition is if the XML has a namespace, but the model does not, or that the namespaces are different. - return ModelDeserializerResult.FromError($"{invalidOperationException.Message} {invalidOperationException.InnerException?.Message}"); + Error = $"{invalidOperationException.Message} {invalidOperationException?.InnerException.Message}"; + return null; } } catch (Exception ex) { - _logger.LogError(ex, "Unexpected exception when attempting to deserialize XML into '{modelType}'", _modelType); - return ModelDeserializerResult.FromError($"Unexpected exception when attempting to deserialize XML into '{_modelType}'"); + string message = $"Unexpected exception when attempting to deserialize XML into '{_modelType}'"; + _logger.LogError(ex, message); + Error = message; + return null; } } @@ -175,4 +157,3 @@ private static string GetRootElementName(Type modelType) } } } - diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializerResult.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializerResult.cs deleted file mode 100644 index 1fec3724d..000000000 --- a/src/Altinn.App.Core/Helpers/Serialization/ModelDeserializerResult.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Altinn.App.Core.Helpers.Serialization; - -/// -/// Result used by to indicate the result type -/// -public class ModelDeserializerResult -{ - /// - /// Static factory method to make an object that represents a successfull deserialization - /// If the model is null, set default error message instead (eg: json string "null", deserialize valid to null without exception) - /// - public static ModelDeserializerResult FromSuccess(object? model, Dictionary? reportedChanges = null) => new() - { - Error = model is null ? "Model deserialzied to \"null\"" : null, - Model = model, - ReportedChanges = model is null ? null : reportedChanges, - }; - /// - /// Static factory method to make an object that represents a failed deserialization - /// - public static ModelDeserializerResult FromError(string error) => new() - { - Error = error, - }; - - // private constructor to ensure that invariant is preserved through static factory methods - private ModelDeserializerResult() { } - - /// - /// Utility function to check if the result has errors and set - /// - /// - /// - /// if(!result.HasError) - /// { - /// //result.Model is not null here - /// } - /// else - /// { - /// //result.Error is not null here - /// } - /// - /// - [MemberNotNullWhen(true, nameof(Error))] - [MemberNotNullWhen(false, nameof(Model))] - public bool HasError => Error is not null; - /// - /// Potential error message, If this is set, the other values are null - /// - public string? Error { get; set; } - - /// - /// The actual parsed model - /// - public object? Model { get; set; } - - /// - /// Dictionary with fields and their changed parts - /// - public Dictionary? ReportedChanges { get; set; } - -} \ No newline at end of file diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index 497e226e4..d72dd04bc 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -167,17 +167,17 @@ public async Task GetFormData(Guid instanceGuid, Type type, string org, HttpResponseMessage response = await _client.GetAsync(token, apiUrl); if (response.IsSuccessStatusCode) { - await using Stream stream = await response.Content.ReadAsStreamAsync(); + using Stream stream = await response.Content.ReadAsStreamAsync(); ModelDeserializer deserializer = new ModelDeserializer(_logger, type); - ModelDeserializerResult deserializerResult = await deserializer.DeserializeAsync(stream, "application/xml"); + object? model = await deserializer.DeserializeAsync(stream, "application/xml"); - if (deserializerResult.HasError) + if (deserializer.Error != null || model is null) { - _logger.LogError("Cannot deserialize XML form data read from storage: {deserializerError}", deserializerResult.Error); - throw new ServiceException(HttpStatusCode.Conflict, $"Cannot deserialize XML form data from storage {deserializerResult.Error}"); + _logger.LogError($"Cannot deserialize XML form data read from storage: {deserializer.Error}"); + throw new ServiceException(HttpStatusCode.Conflict, $"Cannot deserialize XML form data from storage {deserializer.Error}"); } - return deserializerResult.Model; + return model; } throw await PlatformHttpException.CreateAsync(response); diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluator.cs index 6d972cc31..a736adb55 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluator.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluator.cs @@ -102,7 +102,7 @@ public static void RemoveHiddenData(LayoutEvaluatorState state, RowRemovalOption /// /// Return a list of for the given state and dataElementId /// - public static IEnumerable RunLayoutValidationsForRequired(LayoutEvaluatorState state, string dataElementId) + public static List RunLayoutValidationsForRequired(LayoutEvaluatorState state, string dataElementId) { var validationIssues = new List(); @@ -134,7 +134,6 @@ private static void RunLayoutValidationsForRequiredRecurs(List validationIssues.Add(new ValidationIssue() { Severity = ValidationIssueSeverity.Error, - InstanceId = state.GetInstanceContext("instanceId").ToString(), DataElementId = dataElementId, Field = field, Description = $"{field} is required in component with id {context.Component.Id}", diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs index cfc0d0162..c376e5341 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs @@ -179,6 +179,14 @@ public ComponentContext GetComponentContext(string pageName, string componentId, return _dataModel.GetModelData(key, context?.RowIndices); } + /// + /// Get all of the resolved keys (including all possible indexes) from a data model key + /// + public string[] GetResolvedKeys(string key) + { + return _dataModel.GetResolvedKeys(key); + } + /// /// Set the value of a field to null. /// diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs index 389d2517f..db8a88c95 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorStateInitializer.cs @@ -27,7 +27,7 @@ public LayoutEvaluatorStateInitializer(IAppResources appResources, IOptions /// Initialize LayoutEvaluatorState with given Instance, data object and layoutSetId /// - public Task Init(Instance instance, object data, string? layoutSetId, string? gatewayAction = null) + public virtual Task Init(Instance instance, object data, string? layoutSetId, string? gatewayAction = null) { var layouts = _appResources.GetLayoutModel(layoutSetId); return Task.FromResult(new LayoutEvaluatorState(new DataModel(data), layouts, _frontEndSettings, instance, gatewayAction)); diff --git a/src/Altinn.App.Core/Models/Layout/Components/OptionsComponent.cs b/src/Altinn.App.Core/Models/Layout/Components/OptionsComponent.cs index 26e91baf7..b77c574eb 100644 --- a/src/Altinn.App.Core/Models/Layout/Components/OptionsComponent.cs +++ b/src/Altinn.App.Core/Models/Layout/Components/OptionsComponent.cs @@ -52,10 +52,9 @@ public class OptionsSource /// /// Constructor for /// - public OptionsSource(string group, string label, string value) + public OptionsSource(string group, string value) { Group = group; - Label = label; Value = value; } /// @@ -63,36 +62,6 @@ public OptionsSource(string group, string label, string value) /// public string Group { get; } /// - /// a reference to a text id to be used as the label for each iteration of the group - /// - /// - /// As for the label property, we have to define a text resource that can be used as a label for each repetition of the group. - /// This follows similar syntax as the value, and will also be familiar if you have used variables in text. - /// - /// - /// The referenced text resource must use variables to read text from individual fields - /// { - /// "language": "nb", - /// "resources": [ - /// { - /// "id": "dropdown.label", - /// "value": "Person: {0}, Age: {1}", - /// "variables": [ - /// { - /// "key": "some.group[{0}].name", - /// "dataSource": "dataModel.default" - /// }, - /// { - /// "key": "some.group[{0}].age", - /// "dataSource": "dataModel.default" - /// } - /// ] - /// } - /// ] - /// } - /// - public string Label { get; } - /// /// a reference to a field in the group that should be used as the option value. Notice that we set up this [{0}] syntax. Here the {0} will be replaced by each index of the group. /// /// diff --git a/src/Altinn.App.Core/Models/Layout/Components/SummaryComponent.cs b/src/Altinn.App.Core/Models/Layout/Components/SummaryComponent.cs index da2b072b4..b42e08eb8 100644 --- a/src/Altinn.App.Core/Models/Layout/Components/SummaryComponent.cs +++ b/src/Altinn.App.Core/Models/Layout/Components/SummaryComponent.cs @@ -16,19 +16,13 @@ public class SummaryComponent : BaseComponent /// public string ComponentRef { get; set; } - /// - /// Name of the page this summary component references - /// - public string PageRef { get; set; } - /// /// Constructor /// - public SummaryComponent(string id, string type, Expression? hidden, string componentRef, string pageRef, IReadOnlyDictionary? additionalProperties) : + public SummaryComponent(string id, string type, Expression? hidden, string componentRef, IReadOnlyDictionary? additionalProperties) : base(id, type, null, hidden, null, null, additionalProperties) { ComponentRef = componentRef; - PageRef = pageRef; } } diff --git a/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs b/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs index 607bdc391..59f23c33d 100644 --- a/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs +++ b/src/Altinn.App.Core/Models/Layout/PageComponentConverter.cs @@ -234,7 +234,6 @@ private BaseComponent ReadComponent(ref Utf8JsonReader reader, JsonSerializerOpt int maxCount = 1; // > 1 is repeating, but might not be specified for non-repeating groups // Custom properties for Summary string? componentRef = null; - string? pageRef = null; // Custom properties for components with optionId or literal options string? optionId = null; List? literalOptions = null; @@ -294,9 +293,6 @@ private BaseComponent ReadComponent(ref Utf8JsonReader reader, JsonSerializerOpt case "componentref": componentRef = reader.GetString(); break; - case "pageref": - pageRef = reader.GetString(); - break; // option case "optionsid": optionId = reader.GetString(); @@ -321,6 +317,17 @@ private BaseComponent ReadComponent(ref Utf8JsonReader reader, JsonSerializerOpt switch (type.ToLowerInvariant()) { + case "repeatinggroup": + ThrowJsonExceptionIfNull(children, "Component with \"type\": \"Group\" requires a \"children\" property"); + if (!(dataModelBindings?.ContainsKey("group") ?? false)) + { + throw new JsonException($"A repeating group id:\"{id}\" does not have a \"group\" dataModelBinding"); + } + + var directRepComponent = new RepeatingGroupComponent(id, type, dataModelBindings, new List(), children, maxCount, hidden, hiddenRow, required, readOnly, additionalProperties); + return directRepComponent; + + case "group": ThrowJsonExceptionIfNull(children, "Component with \"type\": \"Group\" requires a \"children\" property"); @@ -343,8 +350,8 @@ private BaseComponent ReadComponent(ref Utf8JsonReader reader, JsonSerializerOpt var gridComponent = new GridComponent(id, type, dataModelBindings, new List(), children, hidden, required, readOnly, additionalProperties); return gridComponent; case "summary": - ValidateSummary(componentRef, pageRef); - return new SummaryComponent(id, type, hidden, componentRef, pageRef, additionalProperties); + ValidateSummary(componentRef); + return new SummaryComponent(id, type, hidden, componentRef, additionalProperties); case "checkboxes": case "radiobuttons": case "dropdown": @@ -384,11 +391,11 @@ private static void ValidateOptions(string? optionId, List? literalOp } } - private static void ValidateSummary([NotNull] string? componentRef, [NotNull] string? pageRef) + private static void ValidateSummary([NotNull] string? componentRef) { - if (componentRef is null || pageRef is null) + if (componentRef is null) { - throw new JsonException("Component with \"type\": \"Summary\" requires \"componentRef\" and \"pageRef\" properties"); + throw new JsonException("Component with \"type\": \"Summary\" requires the \"componentRef\" property"); } } diff --git a/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs b/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs index c10b85366..07865ce12 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationIssue.cs @@ -1,5 +1,7 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.App.Core.Features.Validation; using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace Altinn.App.Core.Models.Validation { @@ -11,50 +13,83 @@ public class ValidationIssue /// /// The seriousness of the identified issue. /// + /// + /// This property is serialized in json as a number + /// 1: Error (something needs to be fixed) + /// 2: Warning (does not prevent submission) + /// 3: Information (hint shown to the user) + /// 4: Fixed (obsolete, only used for v3 of frontend) + /// 5: Success (Inform the user that something was completed with success) + /// [JsonProperty(PropertyName = "severity")] - [JsonConverter(typeof(StringEnumConverter))] - public ValidationIssueSeverity Severity { get; set; } + [JsonPropertyName("severity")] + [System.Text.Json.Serialization.JsonConverter(typeof(JsonNumberEnumConverter))] + public required ValidationIssueSeverity Severity { get; set; } /// /// The unique id of the specific element with the identified issue. /// - [JsonProperty(PropertyName = "instanceId")] + [System.Text.Json.Serialization.JsonIgnore] + [Newtonsoft.Json.JsonIgnore] + [Obsolete("Not in use", error: true)] public string? InstanceId { get; set; } /// - /// The uniqe id of the data element of a given instance with the identified issue. + /// The unique id of the data element of a given instance with the identified issue. /// [JsonProperty(PropertyName = "dataElementId")] + [JsonPropertyName("dataElementId")] public string? DataElementId { get; set; } /// - /// A reference to a property the issue is a bout. + /// A reference to a property the issue is about. /// [JsonProperty(PropertyName = "field")] + [JsonPropertyName("field")] public string? Field { get; set; } /// /// A system readable identification of the type of issue. + /// Eg: /// [JsonProperty(PropertyName = "code")] + [JsonPropertyName("code")] public string? Code { get; set; } /// /// A human readable description of the issue. /// [JsonProperty(PropertyName = "description")] + [JsonPropertyName("description")] public string? Description { get; set; } /// - /// The validation source of the issue eg. File, Schema, Component + /// The short name of the class that crated the message (set automatically after return of list) /// + /// + /// Intentionally not marked as "required", because it is set in + /// [JsonProperty(PropertyName = "source")] - public string? Source { get; set; } + [JsonPropertyName("source")] + public string Source { get; set; } = default!; /// /// The custom text key to use for the localized text in the frontend. /// [JsonProperty(PropertyName = "customTextKey")] + [JsonPropertyName("customTextKey")] public string? CustomTextKey { get; set; } + + /// + /// might include some parameters (typically the field value, or some derived value) + /// that should be included in error message. + /// + /// + /// The localized text for the key might be "Date must be between {0} and {1}" + /// and the param will provide the dynamical range of allowable dates (eg teh reporting period) + /// + [JsonProperty(PropertyName = "customTextParams")] + [JsonPropertyName("customTextParams")] + public List? CustomTextParams { get; set; } } } diff --git a/src/Altinn.App.Core/Models/Validation/ValidationIssueSeverity.cs b/src/Altinn.App.Core/Models/Validation/ValidationIssueSeverity.cs index 5661998d7..2b2264492 100644 --- a/src/Altinn.App.Core/Models/Validation/ValidationIssueSeverity.cs +++ b/src/Altinn.App.Core/Models/Validation/ValidationIssueSeverity.cs @@ -28,6 +28,7 @@ public enum ValidationIssueSeverity /// /// The issue has been corrected. /// + [Obsolete("We run all validations from frontend version 4, so we don't need info about fixed issues")] Fixed = 4, /// diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PatchFormDataImplementation.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchFormDataImplementation.cs new file mode 100644 index 000000000..bbf2a97a5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchFormDataImplementation.cs @@ -0,0 +1,156 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Altinn.App.Api.Controllers; +using Altinn.App.Api.Models; +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.FileAnalyzis; +using Altinn.App.Core.Features.Validation; +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.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.FeatureManagement; +using Moq; +using Xunit; +using DataType = Altinn.Platform.Storage.Interface.Models.DataType; + +namespace Altinn.App.Api.Tests.Controllers; + +public class DataController_PatchFormDataImplementation : IAsyncDisposable +{ + // Test data + static readonly Guid DataGuid = new("12345678-1234-1234-1234-123456789123"); + private readonly Instance _instance = new(); + + // Service mocks + private readonly Mock> _dLoggerMock = new(MockBehavior.Loose); + private readonly Mock> _vLoggerMock = new(MockBehavior.Loose); + private readonly Mock _instanceClientMock = new(MockBehavior.Strict); + private readonly Mock _instantiationProcessorMock = new(MockBehavior.Strict); + private readonly Mock _dataClientMock = new (MockBehavior.Strict); + private readonly Mock _dataProcessorMock = new(MockBehavior.Strict); + private readonly Mock _appModelMock = new (MockBehavior.Strict); + private readonly Mock _appResourcesServiceMock = new (MockBehavior.Strict); + private readonly Mock _prefillServiceMock = new (MockBehavior.Strict); + private readonly Mock _fileAnalyserServiceMock = new (MockBehavior.Strict); + private readonly Mock _fileValidationServiceMock = new (MockBehavior.Strict); + private readonly Mock _appMetadataMock = new (MockBehavior.Strict); + private readonly Mock _featureManageMock = new (MockBehavior.Strict); + + // ValidatorMocks + private readonly Mock _formDataValidator = new(MockBehavior.Strict); + private readonly Mock _dataElementValidator = new(MockBehavior.Strict); + + // System under test + private readonly ServiceCollection _serviceCollection = new(); + private readonly DataController _dataController; + private readonly ServiceProvider _serviceProvider; + + public DataController_PatchFormDataImplementation() + { + _formDataValidator.Setup(fdv => fdv.DataType).Returns(_dataType.Id); + _formDataValidator.Setup(fdv => fdv.ValidationSource).Returns("formDataValidator"); + _formDataValidator.Setup(fdv => fdv.HasRelevantChanges(It.IsAny(), It.IsAny())).Returns(true); + // _dataElementValidator.Setup(ev => ev.DataType).Returns(_dataType.Id); + _serviceCollection.AddSingleton(_formDataValidator.Object); + _serviceCollection.AddSingleton(_dataElementValidator); + _serviceProvider = _serviceCollection.BuildServiceProvider(); + var validationService = new ValidationService( + _serviceProvider, + _dataClientMock.Object, + _appModelMock.Object, + _appMetadataMock.Object, + _vLoggerMock.Object + ); + _dataController = new DataController( + _dLoggerMock.Object, + _instanceClientMock.Object, + _instantiationProcessorMock.Object, + _dataClientMock.Object, + new List (){_dataProcessorMock.Object}, + _appModelMock.Object, + _appResourcesServiceMock.Object, + _prefillServiceMock.Object, + validationService, + _fileAnalyserServiceMock.Object, + _fileValidationServiceMock.Object, + _appMetadataMock.Object, + _featureManageMock.Object + ); + } + + private readonly DataType _dataType = new() + { + Id = "dataTypeId", + }; + + private readonly DataElement _dataElement = new() + { + Id = DataGuid.ToString(), + DataType = "dataTypeId" + }; + + private class MyModel + { + [MinLength(20)] + public string? Name { get; set; } + } + + [Fact] + public async Task Test() + { + var request = JsonSerializer.Deserialize(""" + { + "patch": [ + { + "op": "replace", + "path": "/Name", + "value": "Test Testesen" + } + ], + "ignoredValidators": [ + "required" + ] + } + """)!; + var oldModel = new MyModel { Name = "OrginaltNavn" }; + var validationIssues = new List() + { + new () + { + Severity = ValidationIssueSeverity.Error, + Description = "First error", + } + }; + + _dataProcessorMock.Setup(d => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns((Instance i, Guid j, MyModel data, MyModel? oldData) => Task.CompletedTask); + _formDataValidator.Setup(fdv => fdv.ValidateFormData( + It.Is(i => i == _instance), + It.Is(de=>de == _dataElement), + It.IsAny())) + .ReturnsAsync(validationIssues); + + // Act + var (response, _) = await _dataController.PatchFormDataImplementation(_dataType, _dataElement, request, oldModel, _instance); + + // Assert + response.Should().NotBeNull(); + response.NewDataModel.Should().BeOfType().Subject.Name.Should().Be("Test Testesen"); + var validator = response.ValidationIssues.Should().ContainSingle().Which; + validator.Key.Should().Be("formDataValidator"); + var issue = validator.Value.Should().ContainSingle().Which; + issue.Description.Should().Be("First error"); + _dataProcessorMock.Verify(d => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + } + + public async ValueTask DisposeAsync() + { + await _serviceProvider.DisposeAsync(); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs new file mode 100644 index 000000000..cefc9e274 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs @@ -0,0 +1,343 @@ +using Altinn.App.Api.Tests.Utils; +using Microsoft.AspNetCore.Mvc.Testing; +using System.Net.Http.Headers; +using System.Net; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Altinn.App.Api.Models; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; +using Altinn.App.Core.Features; +using Xunit; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Json.More; +using Json.Patch; +using Json.Pointer; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit.Abstractions; + +namespace Altinn.App.Api.Tests.Controllers; + +public class DataControllerPatchTests : ApiTestBase, IClassFixture> +{ + // Define constants + private const string Org = "tdd"; + private const string App = "contributer-restriction"; + private const int InstanceOwnerPartyId = 500600; + private static readonly Guid InstanceGuid = new("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); + private static readonly string InstanceId = $"{InstanceOwnerPartyId}/{InstanceGuid}"; + private static readonly Guid DataGuid = new("fc121812-0336-45fb-a75c-490df3ad5109"); + + // Define mocks + private readonly Mock _dataProcessorMock = new(); + private readonly Mock _formDataValidatorMock = new(); + + private static readonly JsonSerializerOptions JsonSerializerOptions = new () + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + + private readonly ITestOutputHelper _outputHelper; + + // Constructor with common setup + public DataControllerPatchTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) : base(factory) + { + _formDataValidatorMock.Setup(v => v.DataType).Returns("Not a valid data type"); + _outputHelper = outputHelper; + OverrideServicesForAllTests = (services) => + { + services.AddSingleton(_dataProcessorMock.Object); + services.AddSingleton(_formDataValidatorMock.Object); + }; + TestData.DeleteInstanceAndData(Org, App, InstanceOwnerPartyId, InstanceGuid); + TestData.PrepareInstance(Org, App, InstanceOwnerPartyId, InstanceGuid); + } + + // Helper method to call the API + private async Task<(HttpResponseMessage response, string responseString, TResponse parsedResponse)> CallPatchApi(JsonPatch patch, List? ignoredValidators, HttpStatusCode expectedStatus) + { + _outputHelper.WriteLine($"Calling PATCH /{Org}/{App}/instances/{InstanceId}/data/{DataGuid}"); + using var httpClient = GetRootedClient(Org, App); + string token = PrincipalUtil.GetToken(1337, null); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + var serializedPatch = JsonSerializer.Serialize(new DataPatchRequest() + { + Patch = patch, + IgnoredValidators = ignoredValidators, + }, JsonSerializerOptions); + _outputHelper.WriteLine(serializedPatch); + using var updateDataElementContent = + new StringContent(serializedPatch, System.Text.Encoding.UTF8, "application/json"); + var response = await httpClient.PatchAsync($"/{Org}/{App}/instances/{InstanceId}/data/{DataGuid}", updateDataElementContent); + var responseString = await response.Content.ReadAsStringAsync(); + using var responseParsedRaw = JsonDocument.Parse(responseString); + _outputHelper.WriteLine("\nResponse:"); + _outputHelper.WriteLine(JsonSerializer.Serialize(responseParsedRaw, JsonSerializerOptions)); + response.Should().HaveStatusCode(expectedStatus); + var responseObject = JsonSerializer.Deserialize(responseString, JsonSerializerOptions)!; + return (response, responseString, responseObject); + } + + + [Fact] + public async Task ValidName_ReturnsOk() + { + // Update data element + var patch = new JsonPatch( + PatchOperation.Replace(JsonPointer.Create("melding", "name"), JsonNode.Parse("\"Ola Olsen\""))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.OK); + + parsedResponse.ValidationIssues.Should().ContainKey("Required").WhoseValue.Should().BeEmpty(); + + var newModelElement = parsedResponse.NewDataModel.Should().BeOfType().Which; + var newModel = newModelElement.Deserialize()!; + newModel.Melding.Name.Should().Be("Ola Olsen"); + + _dataProcessorMock.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == DataGuid), It.IsAny(), It.IsAny()), Times.Exactly(1)); + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task NullName_ReturnsOkAndValidationError() + { + // Update data element + var patch = new JsonPatch( + PatchOperation.Test(JsonPointer.Create("melding", "name"), JsonNode.Parse("null")), + PatchOperation.Replace(JsonPointer.Create("melding", "name"), JsonNode.Parse("null"))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.OK); + + var requiredList = parsedResponse.ValidationIssues.Should().ContainKey("Required").WhoseValue; + var requiredName = requiredList.Should().ContainSingle().Which; + requiredName.Field.Should().Be("melding.name"); + requiredName.Description.Should().Be("melding.name is required in component with id name"); + + var newModelElement = parsedResponse.NewDataModel.Should().BeOfType().Which; + var newModel = newModelElement.Deserialize()!; + newModel.Melding.Name.Should().BeNull(); + + _dataProcessorMock.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == DataGuid), It.IsAny(), It.IsAny()), Times.Exactly(1)); + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task InvalidTestValue_ReturnsPreconditionFailed() + { + // Update data element + var patch = new JsonPatch( + PatchOperation.Test(JsonPointer.Create("melding", "name"), JsonNode.Parse("\"Not correct previous value\"")), + PatchOperation.Replace(JsonPointer.Create("melding", "name"), JsonNode.Parse("null"))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.PreconditionFailed); + + parsedResponse.Detail.Should().Be("Path `/melding/name` is not equal to the indicated value."); + + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task InvalidTestPath_ReturnsPreconditionFailed() + { + // Update data element + var patch = new JsonPatch( + PatchOperation.Test(JsonPointer.Create("melding", "name-error"), JsonNode.Parse("null")), + PatchOperation.Replace(JsonPointer.Create("melding", "name"), JsonNode.Parse("null"))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.UnprocessableContent); + + parsedResponse.Detail.Should().Be("Path `/melding/name-error` could not be reached."); + + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task InvalidJsonPointer_ReturnsUnprocessableContent() + { + // Update data element + var pointer = JsonPointer.Create("not", "a pointer"); + var patch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("null")), + PatchOperation.Replace(pointer, JsonNode.Parse("\"Ivar\""))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.UnprocessableContent); + + parsedResponse.Detail.Should().Be("Path `/not/a pointer` could not be reached."); + + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task TestEmptyListAndInsertElement_ReturnsNewModel() + { + // Update data element + var pointer = JsonPointer.Create("melding", "nested_list"); + var patch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("""[]""")), + PatchOperation.Add(pointer, JsonNode.Parse("""[{"key": "newKey"}]"""))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.OK); + + var newModel = parsedResponse.NewDataModel.Should().BeOfType().Which.Deserialize()!; + var listItem = newModel.Melding.NestedList.Should().ContainSingle().Which; + listItem.Key.Should().Be("newKey"); + + parsedResponse.ValidationIssues + .Should().ContainKey("Required").WhoseValue + .Should().Contain(i => i.Field == "melding.name"); + + _dataProcessorMock.Verify( + p => p.ProcessDataWrite( + It.IsAny(), + It.Is(dataId => dataId == DataGuid), + It.Is(s=>s.Melding.NestedList.Count == 1), + It.Is(s=> s!.Melding.NestedList.Count == 0) + ), Times.Exactly(1)); + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task AddItemToNonInitializedList_ReturnsUnprocessableEntity() + { + // This test fails to initialize the list, thus creating an error + // Added this test to ensure that a change in behaviour (when changing json patch library) + // is detected + var pointer = JsonPointer.Create("melding", "nested_list", 0, "newKey"); + var patch = new JsonPatch( + PatchOperation.Add(pointer, JsonNode.Parse("\"newValue\""))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.UnprocessableContent); + + parsedResponse.Detail.Should().Contain("/melding/nested_list/0/newKey"); + + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task InsertNonExistingFieldWithoutTest_ReturnsUnprocessableContent() + { + // Update data element + var pointer = JsonPointer.Create("melding", "non_existing_field"); + var patch = new JsonPatch( + PatchOperation.Add(pointer, JsonNode.Parse("""[{"key": "newKey"}]"""))); + + var (_, _, parsedResponse) = await CallPatchApi(patch, null, HttpStatusCode.UnprocessableContent); + + parsedResponse.Detail.Should().Contain("The JSON property 'non_existing_field' could not be mapped to any .NET member contained in type"); + + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task UpdateContainerWithListProperty_ReturnsCorrectDataModel() + { + var pointer = JsonPointer.Create("melding", "nested_list"); + var createFirstElementPatch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("[]")), + PatchOperation.Add(pointer.Combine("-"), JsonNode.Parse("""{"key": "myKey" }""")) + ); + + var (_, _, firstResponse) = await CallPatchApi(createFirstElementPatch, null, HttpStatusCode.OK); + + var firstData = firstResponse.NewDataModel.Should().BeOfType().Which; + var firstListItem = firstData.GetProperty("melding").GetProperty("nested_list").EnumerateArray().First(); + firstListItem.GetProperty("values").GetArrayLength().Should().Be(0); + + var addValuePatch = new JsonPatch( + PatchOperation.Test(pointer.Combine("0"), firstListItem.AsNode()), + PatchOperation.Remove(pointer.Combine("0"))); + var (_, _, secondResponse) = await CallPatchApi(addValuePatch, null, HttpStatusCode.OK); + var secondData = secondResponse.NewDataModel.Should().BeOfType().Which; + secondData.GetProperty("melding").GetProperty("nested_list").GetArrayLength().Should().Be(0); + } + + [Fact] + public async Task RemoveStringProperty_ReturnsCorrectDataModel() + { + var pointer = JsonPointer.Create("melding", "name"); + var createFirstElementPatch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("null")), + PatchOperation.Add(pointer, JsonNode.Parse("\"myValue\"")), + PatchOperation.Remove(pointer) + ); + + var (_, _, firstResponse) = await CallPatchApi(createFirstElementPatch, null, HttpStatusCode.OK); + + var firstData = firstResponse.NewDataModel.Should().BeOfType().Which; + var firstListItem = firstData.GetProperty("melding").GetProperty("name"); + firstListItem.ValueKind.Should().Be(JsonValueKind.Null); + + var addValuePatch = new JsonPatch( + PatchOperation.Test(pointer, firstListItem.AsNode()), + PatchOperation.Replace(pointer, JsonNode.Parse("\"mySecondValue\""))); + var (_, _, secondResponse) = await CallPatchApi(addValuePatch, null, HttpStatusCode.OK); + var secondData = secondResponse.NewDataModel.Should().BeOfType().Which; + var secondValue = secondData.GetProperty("melding").GetProperty("name"); + secondValue.GetString().Should().Be("mySecondValue"); + } + + [Fact] + public async Task SetStringPropertyToEmtpy_ReturnsCorrectDataModel() + { + var pointer = JsonPointer.Create("melding", "name"); + var createFirstElementPatch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("null")), + PatchOperation.Add(pointer, JsonNode.Parse("\"\"")) + ); + + var (_, _, firstResponse) = await CallPatchApi(createFirstElementPatch, null, HttpStatusCode.OK); + + var firstData = firstResponse.NewDataModel.Should().BeOfType().Which; + var firstListItem = firstData.GetProperty("melding").GetProperty("name"); + firstListItem.ValueKind.Should().Be(JsonValueKind.Null);; + + var addValuePatch = new JsonPatch( + PatchOperation.Test(pointer, firstListItem.AsNode()), + PatchOperation.Replace(pointer, JsonNode.Parse("\"mySecondValue\""))); + var (_, _, secondResponse) = await CallPatchApi(addValuePatch, null, HttpStatusCode.OK); + var secondData = secondResponse.NewDataModel.Should().BeOfType().Which; + var secondValue = secondData.GetProperty("melding").GetProperty("name"); + secondValue.GetString().Should().Be("mySecondValue"); + } + + [Fact] + public async Task SetAttributeTagPropertyToEmtpy_ReturnsCorrectDataModel() + { + var pointer = JsonPointer.Create("melding", "tag-with-attribute"); + var createFirstElementPatch = new JsonPatch( + PatchOperation.Test(pointer, JsonNode.Parse("null")), + PatchOperation.Add(pointer, JsonNode.Parse("""{"value": "" }""")) + ); + + var (_, _, firstResponse) = await CallPatchApi(createFirstElementPatch, null, HttpStatusCode.OK); + + var firstData = firstResponse.NewDataModel.Should().BeOfType().Which; + var firstListItem = firstData.GetProperty("melding").GetProperty("tag-with-attribute"); + firstListItem.GetProperty("value").ValueKind.Should().Be(JsonValueKind.Null); + + var addValuePatch = new JsonPatch( + PatchOperation.Test(pointer, firstListItem.AsNode()), + PatchOperation.Replace(pointer.Combine("value"), JsonNode.Parse("null"))); + var (_, _, secondResponse) = await CallPatchApi(addValuePatch, null, HttpStatusCode.OK); + var secondData = secondResponse.NewDataModel.Should().BeOfType().Which; + var secondValue = secondData.GetProperty("melding").GetProperty("name"); + secondValue.ValueKind.Should().Be(JsonValueKind.Null); + } + + [Fact] + public async Task ValidationIssueSeverity_IsSerializedNumeric() + { + var patch = new JsonPatch(); + var (_, responseString, _) = await CallPatchApi(patch, null, HttpStatusCode.OK); + + responseString.Should().Contain("\"severity\":1"); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs index 77906093c..f04fea560 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs @@ -62,7 +62,7 @@ public async Task PutDataElement_TestSinglePartUpdate_ReturnsOk() // Update data element using var updateDataElementContent = - new StringContent("""{"melding":{"name": "Ivar Nesje"}}""", System.Text.Encoding.UTF8, "application/json"); + new StringContent("""{"melding":{"name": "Ola Olsen"}}""", System.Text.Encoding.UTF8, "application/json"); var response = await client.PutAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}", updateDataElementContent); response.StatusCode.Should().Be(HttpStatusCode.Created); @@ -72,67 +72,10 @@ public async Task PutDataElement_TestSinglePartUpdate_ReturnsOk() var readDataElementResponseContent = await readDataElementResponse.Content.ReadAsStringAsync(); var readDataElementResponseParsed = JsonSerializer.Deserialize(readDataElementResponseContent)!; - readDataElementResponseParsed.Melding.Name.Should().Be("Ivar Nesje"); + readDataElementResponseParsed.Melding.Name.Should().Be("Ola Olsen"); _dataProcessor.Verify(p => p.ProcessDataRead(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny()), Times.Exactly(1)); - _dataProcessor.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny(), It.IsAny>()), Times.Exactly(1)); // TODO: Shouldn't this be 2 because of the first write? - _dataProcessor.VerifyNoOtherCalls(); - } - - [Fact] - public async Task PutDataElement_TestMultiPartUpdate_ReturnsOk() - { - // Setup test data - string org = "tdd"; - string app = "contributer-restriction"; - int instanceOwnerPartyId = 501337; - HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetToken(1337, null); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - - // Create instance - var createResponse = - await client.PostAsync($"{org}/{app}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", null); - var createResponseContent = await createResponse.Content.ReadAsStringAsync(); - createResponse.StatusCode.Should().Be(HttpStatusCode.Created); - var createResponseParsed = JsonSerializer.Deserialize(createResponseContent, JsonSerializerOptions)!; - var instanceId = createResponseParsed.Id; - - // Create data element (not sure why it isn't created when the instance is created, autoCreate is true) - using var createDataElementContent = - new StringContent("""{"melding":{"name": "Ivar"}}""", System.Text.Encoding.UTF8, "application/json"); - var createDataElementResponse = - await client.PostAsync($"/{org}/{app}/instances/{instanceId}/data?dataType=default", - createDataElementContent); - var createDataElementResponseContent = await createDataElementResponse.Content.ReadAsStringAsync(); - createDataElementResponse.StatusCode.Should().Be(HttpStatusCode.Created); - var createDataElementResponseParsed = - JsonSerializer.Deserialize(createDataElementResponseContent, JsonSerializerOptions)!; - var dataGuid = createDataElementResponseParsed.Id; - - // Update data element - using var updateDataElementContent = new MultipartFormDataContent(); - updateDataElementContent.Add(new StringContent("""{"melding":{"name": "Ivar Nesje"}}""", System.Text.Encoding.UTF8, - "application/json"), "dataModel"); - updateDataElementContent.Add(new StringContent("""{"melding.name":"Ivar"}""", System.Text.Encoding.UTF8, - "application/json"), "previousValues"); - - var response = await client.PutAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}", updateDataElementContent); - var responseContent = await response.Content.ReadAsStringAsync(); - response.StatusCode.Should().Be(HttpStatusCode.Created); - - // Verify stored data - var readDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); - var readDataElementResponseContent = await readDataElementResponse.Content.ReadAsStringAsync(); - var readDataElementResponseParsed = - JsonSerializer.Deserialize(readDataElementResponseContent)!; - readDataElementResponseParsed.Melding.Name.Should().Be("Ivar Nesje"); - - // Verify that update response equals the following read response - // responseContent.Should().Be(readDataElementResponseContent); - - _dataProcessor.Verify(p=>p.ProcessDataRead(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny()), Times.Exactly(1)); - _dataProcessor.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny(), It.Is>(d => d.ContainsKey("melding.name"))), Times.Exactly(1)); // TODO: Shouldn't this be 2 because of the first write? + _dataProcessor.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny(), It.IsAny()), Times.Exactly(1)); // TODO: Shouldn't this be 2 because of the first write? _dataProcessor.VerifyNoOtherCalls(); } @@ -140,8 +83,8 @@ await client.PostAsync($"/{org}/{app}/instances/{instanceId}/data?dataType=defau public async Task PutDataElement_TestMultiPartUpdateWithCustomDataProcessor_ReturnsOk() { // Run the previous test with a custom data processor - _dataProcessor.Setup(d => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>())) - .Returns((Instance instance, Guid dataGuid, object data, Dictionary? previousValues) => + _dataProcessor.Setup(d => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((Instance instance, Guid dataGuid, object data, object previousData) => { if (data is Skjema skjema) { @@ -189,26 +132,22 @@ await client.PostAsync($"/{org}/{app}/instances/{instanceId}/data?dataType=defau firstReadDataElementResponseParsed.Melding.Toggle.Should().BeFalse(); // Update data element - using var updateDataElementContent = new MultipartFormDataContent(); - updateDataElementContent.Add(new StringContent("""{"melding":{"name": "Ivar Nesje"}}""", System.Text.Encoding.UTF8, - "application/json"), "\"dataModel\""); - updateDataElementContent.Add(new StringContent("""{"melding.name":"Ivar"}""", System.Text.Encoding.UTF8, - "application/json"), "\"previousValues\""); - + using var updateDataElementContent = + new StringContent("""{"melding":{"name": "Ola Olsen"}}""", System.Text.Encoding.UTF8, "application/json"); var response = await client.PutAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}", updateDataElementContent); - var responseContent = await response.Content.ReadAsStringAsync(); response.StatusCode.Should().Be(HttpStatusCode.SeeOther); + // Verify stored data var readDataElementResponse = await client.GetAsync($"/{org}/{app}/instances/{instanceId}/data/{dataGuid}"); var readDataElementResponseContent = await readDataElementResponse.Content.ReadAsStringAsync(); var readDataElementResponseParsed = JsonSerializer.Deserialize(readDataElementResponseContent)!; - readDataElementResponseParsed.Melding.Name.Should().Be("Ivar Nesje"); + readDataElementResponseParsed.Melding.Name.Should().Be("Ola Olsen"); readDataElementResponseParsed.Melding.Toggle.Should().BeTrue(); _dataProcessor.Verify(p=>p.ProcessDataRead(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny()), Times.Exactly(2)); - _dataProcessor.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny(), It.Is>(d => d.ContainsKey("melding.name"))), Times.Exactly(1)); // TODO: Shouldn't this be 2 because of the first write? + _dataProcessor.Verify(p => p.ProcessDataWrite(It.IsAny(), It.Is(dataId => dataId == Guid.Parse(dataGuid)), It.IsAny(), It.IsAny()), Times.Exactly(1)); // TODO: Shouldn't this be 2 because of the first write? _dataProcessor.VerifyNoOtherCalls(); } diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs index 909bc8360..22407bb7e 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs @@ -233,7 +233,7 @@ public async Task KnownUser_ReturnsUserName() LastChanged = i.LastChanged, LastChangedBy = i.LastChangedBy switch { - "12345" => "Ola Nordmann", + "12345" => "Ola Olsen", _ => throw new Exception("Unknown user"), } }); @@ -243,7 +243,7 @@ public async Task KnownUser_ReturnsUserName() { Party = new() { - Name = "Ola Nordmann" + Name = "Ola Olsen" } }); diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs index 09bf34d14..75e56fcfa 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerTests.cs @@ -1,5 +1,6 @@ using System.Net; using Altinn.App.Api.Controllers; +using Altinn.App.Core.Features; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; @@ -21,7 +22,7 @@ public async Task ValidateInstance_returns_NotFound_when_GetInstance_returns_nul // Arrange var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); const string org = "ttd"; const string app = "app"; @@ -45,7 +46,7 @@ public async Task ValidateInstance_throws_ValidationException_when_Instance_Proc // Arrange var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); const string org = "ttd"; const string app = "app"; @@ -77,7 +78,7 @@ public async Task ValidateInstance_throws_ValidationException_when_Instance_Proc // Arrange var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); const string org = "ttd"; const string app = "app"; @@ -112,7 +113,7 @@ public async Task ValidateInstance_returns_OK_with_messages() // Arrange var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); const string org = "ttd"; const string app = "app"; @@ -143,7 +144,7 @@ public async Task ValidateInstance_returns_OK_with_messages() instanceMock.Setup(i => i.GetInstance(app, org, instanceOwnerPartyId, instanceId)) .Returns(Task.FromResult(instance)); - validationMock.Setup(v => v.ValidateAndUpdateProcess(instance, "dummy")) + validationMock.Setup(v => v.ValidateInstanceAtTask(instance, "dummy")) .Returns(Task.FromResult(validationResult)); // Act @@ -161,7 +162,7 @@ public async Task ValidateInstance_returns_403_when_not_authorized() // Arrange var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); const string org = "ttd"; const string app = "app"; @@ -186,7 +187,7 @@ public async Task ValidateInstance_returns_403_when_not_authorized() instanceMock.Setup(i => i.GetInstance(app, org, instanceOwnerPartyId, instanceId)) .Returns(Task.FromResult(instance)); - validationMock.Setup(v => v.ValidateAndUpdateProcess(instance, "dummy")) + validationMock.Setup(v => v.ValidateInstanceAtTask(instance, "dummy")) .Throws(exception); // Act @@ -204,7 +205,7 @@ public async Task ValidateInstance_throws_PlatformHttpException_when_not_403() // Arrange var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); const string org = "ttd"; const string app = "app"; @@ -229,7 +230,7 @@ public async Task ValidateInstance_throws_PlatformHttpException_when_not_403() instanceMock.Setup(i => i.GetInstance(app, org, instanceOwnerPartyId, instanceId)) .Returns(Task.FromResult(instance)); - validationMock.Setup(v => v.ValidateAndUpdateProcess(instance, "dummy")) + validationMock.Setup(v => v.ValidateInstanceAtTask(instance, "dummy")) .Throws(exception); // Act diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs index 2e76cc67c..b09672457 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateControllerValidateDataTests.cs @@ -1,5 +1,6 @@ using System.Collections; using Altinn.App.Api.Controllers; +using Altinn.App.Core.Features; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; using Altinn.App.Core.Infrastructure.Clients; @@ -124,7 +125,6 @@ public class TestScenariosData : IEnumerable new ValidationIssue { Code = ValidationIssueCodes.DataElementCodes.DataElementValidatedAtWrongTask, - InstanceId = "0fc98a23-fe31-4ef5-8fb9-dd3f479354ef", Severity = ValidationIssueSeverity.Warning, DataElementId = "0fc98a23-fe31-4ef5-8fb9-dd3f479354cd", Description = AppTextHelper.GetAppText( @@ -236,18 +236,18 @@ public async Task TestValidateData(ValidateDataTestScenario testScenario) private static ValidateController SetupController(string app, string org, int instanceOwnerId, ValidateDataTestScenario testScenario) { - (Mock instanceMock, Mock appResourceMock, Mock validationMock) = + (Mock instanceMock, Mock appResourceMock, Mock validationMock) = SetupMocks(app, org, instanceOwnerId, testScenario); return new ValidateController(instanceMock.Object, validationMock.Object, appResourceMock.Object); } - private static (Mock, Mock, Mock) SetupMocks(string app, string org, + private static (Mock, Mock, Mock) SetupMocks(string app, string org, int instanceOwnerId, ValidateDataTestScenario testScenario) { var instanceMock = new Mock(); var appMetadataMock = new Mock(); - var validationMock = new Mock(); + var validationMock = new Mock(); if (testScenario.ReceivedInstance != null) { instanceMock.Setup(i => i.GetInstance(app, org, instanceOwnerId, testScenario.InstanceId)) @@ -263,8 +263,8 @@ private static (Mock, Mock, Mock) Se { validationMock.Setup(v => v.ValidateDataElement( testScenario.ReceivedInstance, - testScenario.ReceivedApplication.DataTypes.First(), - testScenario.ReceivedInstance.Data.First())) + testScenario.ReceivedInstance.Data.First(), + testScenario.ReceivedApplication.DataTypes.First())) .Returns(Task.FromResult>(testScenario.ReceivedValidationIssues)); } diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs new file mode 100644 index 000000000..aedca21c9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs @@ -0,0 +1,138 @@ +using Altinn.App.Api.Tests.Utils; +using Microsoft.AspNetCore.Mvc.Testing; +using System.Net.Http.Headers; +using System.Net; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Altinn.App.Api.Models; +using Altinn.App.Api.Tests.Data; +using Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; +using Altinn.App.Core.Features; +using Altinn.App.Core.Models.Validation; +using Xunit; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Json.Patch; +using Json.Pointer; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit.Abstractions; + +namespace Altinn.App.Api.Tests.Controllers; + +public class ValidateControllerValidateInstanceTests : ApiTestBase, IClassFixture> +{ + private const string Org = "tdd"; + private const string App = "contributer-restriction"; + private const int InstanceOwnerPartyId = 500600; + private static readonly Guid InstanceGuid = new("3102f61d-1446-4ca5-9fed-3c7c7d67249c"); + private static readonly string InstanceId = $"{InstanceOwnerPartyId}/{InstanceGuid}"; + private static readonly Guid DataGuid = new("5240d834-dca6-44d3-b99a-1b7ca9b862af"); + + private readonly Mock _dataProcessorMock = new(); + private readonly Mock _formDataValidatorMock = new(); + + private static readonly JsonSerializerOptions JsonSerializerOptions = new () + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + + private readonly ITestOutputHelper _outputHelper; + + public ValidateControllerValidateInstanceTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) : base(factory) + { + _formDataValidatorMock.Setup(v => v.DataType).Returns("Not a valid data type"); + _outputHelper = outputHelper; + OverrideServicesForAllTests = (services) => + { + services.AddSingleton(_dataProcessorMock.Object); + services.AddSingleton(_formDataValidatorMock.Object); + }; + TestData.DeleteInstanceAndData(Org, App, InstanceOwnerPartyId, InstanceGuid); + TestData.PrepareInstance(Org, App, InstanceOwnerPartyId, InstanceGuid); + } + + private async Task CallValidateInstanceApi() + { + using var httpClient = GetRootedClient(Org, App); + string token = PrincipalUtil.GetToken(1337, null); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + return await httpClient.GetAsync($"/{Org}/{App}/instances/{InstanceId}/validate"); + } + + private async Task<(HttpResponseMessage response, string responseString)> CallValidateDataApi() + { + using var httpClient = GetRootedClient(Org, App); + string token = PrincipalUtil.GetToken(1337, null); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + var response = await httpClient.GetAsync($"/{Org}/{App}/instances/{InstanceId}/data/{DataGuid}/validate"); + var responseString = await LogResponse(response); + return (response, responseString); + } + + private async Task LogResponse(HttpResponseMessage response) + { + var responseString = await response.Content.ReadAsStringAsync(); + using var responseParsedRaw = JsonDocument.Parse(responseString); + _outputHelper.WriteLine(JsonSerializer.Serialize(responseParsedRaw, JsonSerializerOptions)); + return responseString; + + } + private static TResponse ParseResponse(string responseString) + { + return JsonSerializer.Deserialize(responseString, JsonSerializerOptions)!; + } + + [Fact] + public async Task ValidateInstance_NoSetup() + { + var response = await CallValidateInstanceApi(); + var responseString = await LogResponse(response); + + response.Should().HaveStatusCode(HttpStatusCode.OK); + var parsedResponse = ParseResponse>(responseString); + parsedResponse.Should().BeEmpty(); + + _dataProcessorMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ValidateInstance_WithTaskValidator() + { + var oldTaskValidatorMock = new Mock(MockBehavior.Strict); + + oldTaskValidatorMock.Setup(v => v.ValidateTask(It.IsAny(), "Task_1", It.IsAny())) + .Returns( + (Instance instance, string task, ModelStateDictionary issues) => + { + issues.AddModelError((Skjema s)=>s.Melding.NestedList, "CustomErrorText"); + return Task.CompletedTask; + }).Verifiable(Times.Once); + + OverrideServicesForThisTest = (services) => + { + services.AddSingleton(oldTaskValidatorMock.Object); + }; + var response = await CallValidateInstanceApi(); + var responseString = await LogResponse(response); + + response.Should().HaveStatusCode(HttpStatusCode.OK); + var parsedResponse = ParseResponse>(responseString); + var singleIssue = parsedResponse.Should().ContainSingle().Which; + singleIssue.Field.Should().BeNull(); + singleIssue.Code.Should().Be("CustomErrorText"); + singleIssue.Severity.Should().Be(ValidationIssueSeverity.Error); + + _dataProcessorMock.VerifyNoOtherCalls(); + oldTaskValidatorMock.Verify(); + oldTaskValidatorMock.VerifyNoOtherCalls(); + } + +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/.gitignore b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/.gitignore index 9c7e6c4af..dcf6ac329 100644 --- a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/.gitignore +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/.gitignore @@ -1,6 +1,4 @@ -#Ignore all blob files named with a guid for the dataElementId -????????-????-????-????-???????????? -# Ignore json files -*.json -# Except those that ends in .pretest.json -!*.pretest.json \ No newline at end of file +# Ignore guid.json files +????????-????-????-????-????????????.json +# ignore copied blobs +*/*/blob/????????-????-????-????-???????????? \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd.pretest.json new file mode 100644 index 000000000..1cc0c5045 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd.pretest.json @@ -0,0 +1,30 @@ +{ + "id": "500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd", + "instanceOwner": { + "partyId": "500600", + "organisationNumber": "897069631" + }, + "appId": "tdd/contributer-restriction", + "org": "tdd", + "process": { + "started": "2019-12-05T13:24:34.8412179Z", + "startEvent": "StartEvent_1", + "currentTask": { + "flow": 2, + "started": "2019-12-05T13:24:34.9196661Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "validated": { + "timestamp": "2020-02-07T10:46:36.985894Z", + "canCompleteTask": false + } + } + }, + "status": { + "isArchived": false, + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/blob/fc121812-0336-45fb-a75c-490df3ad5109.pretest b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/blob/fc121812-0336-45fb-a75c-490df3ad5109.pretest new file mode 100644 index 000000000..903c3e28a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/blob/fc121812-0336-45fb-a75c-490df3ad5109.pretest @@ -0,0 +1,6 @@ + + + + false + + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/fc121812-0336-45fb-a75c-490df3ad5109.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/fc121812-0336-45fb-a75c-490df3ad5109.pretest.json new file mode 100644 index 000000000..bc6bc73e5 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/0fc98a23-fe31-4ef5-8fb9-dd3f479354cd/fc121812-0336-45fb-a75c-490df3ad5109.pretest.json @@ -0,0 +1,22 @@ +{ + "id": "fc121812-0336-45fb-a75c-490df3ad5109", + "instanceGuid": "0fc98a23-fe31-4ef5-8fb9-dd3f479354cd", + "dataType": "default", + "filename": null, + "contentType": "application/xml", + "blobStoragePath": null, + "selfLinks": null, + "size": 0, + "contentHash": null, + "locked": false, + "refs": null, + "isRead": true, + "tags": [], + "deleteStatus": null, + "fileScanResult": "NotApplicable", + "references": null, + "created": null, + "createdBy": null, + "lastChanged": "2024-01-10T22:04:31.511965Z", + "lastChangedBy": null +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c.pretest.json new file mode 100644 index 000000000..5b305331b --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c.pretest.json @@ -0,0 +1,30 @@ +{ + "id": "500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c", + "instanceOwner": { + "partyId": "500600", + "organisationNumber": "897069631" + }, + "appId": "tdd/contributer-restriction", + "org": "tdd", + "process": { + "started": "2019-12-05T13:24:34.8412179Z", + "startEvent": "StartEvent_1", + "currentTask": { + "flow": 2, + "started": "2019-12-05T13:24:34.9196661Z", + "elementId": "Task_1", + "name": "Utfylling", + "altinnTaskType": "data", + "validated": { + "timestamp": "2020-02-07T10:46:36.985894Z", + "canCompleteTask": false + } + } + }, + "status": { + "isArchived": false, + "isSoftDeleted": false, + "isHardDeleted": false, + "readStatus": "Read" + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest.json b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest.json new file mode 100644 index 000000000..c63a52a95 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest.json @@ -0,0 +1,22 @@ +{ + "id": "5240d834-dca6-44d3-b99a-1b7ca9b862af", + "instanceGuid": "3102f61d-1446-4ca5-9fed-3c7c7d67249c", + "dataType": "default", + "filename": null, + "contentType": "application/xml", + "blobStoragePath": null, + "selfLinks": null, + "size": 0, + "contentHash": null, + "locked": false, + "refs": null, + "isRead": true, + "tags": [], + "deleteStatus": null, + "fileScanResult": "NotApplicable", + "references": null, + "created": null, + "createdBy": null, + "lastChanged": "2024-01-10T22:04:31.511965Z", + "lastChangedBy": null +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/blob/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/blob/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest new file mode 100644 index 000000000..2f4a2b54a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Instances/tdd/contributer-restriction/500600/3102f61d-1446-4ca5-9fed-3c7c7d67249c/blob/5240d834-dca6-44d3-b99a-1b7ca9b862af.pretest @@ -0,0 +1,7 @@ + + + + Per Olsen + false + + \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/appsettings.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/appsettings.json index 03bd1e942..3a241fba2 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/appsettings.json +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/appsettings.json @@ -7,7 +7,9 @@ } }, "AppSettings": { - "RuntimeCookieName": "AltinnStudioRuntime" + "RuntimeCookieName": "AltinnStudioRuntime", + "RequiredValidation": true, + "ExpressionValidation": true }, "GeneralSettings": { "HostName": "altinn3.no", diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs index 877479524..8ad9fe390 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using System.Xml.Serialization; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json; namespace Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; @@ -44,6 +45,24 @@ public class Dummy [JsonProperty("toggle")] [JsonPropertyName("toggle")] public bool Toggle { get; set; } = default!; + + [XmlElement("tag-with-attribute", IsNullable = true, Order = 7)] + [JsonProperty("tag-with-attribute")] + [JsonPropertyName("tag-with-attribute")] + public TagWithAttribute TagWithAttribute { get; set; } = default!; +} + +public class TagWithAttribute +{ + [Range(1, Int32.MaxValue)] + [XmlAttribute("orid")] + [BindNever] + public decimal orid { get; set; } = 34730; + + [MinLength(1)] + [MaxLength(60)] + [XmlText()] + public string? value { get; set; } } public class ValuesList diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/Settings.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/Settings.json new file mode 100644 index 000000000..7e63e1852 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/Settings.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://altinncdn.no/schemas/json/layout/layoutSettings.schema.v1.json", + "pages": { + "order": [ + "page" + ] + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/layouts/page.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/layouts/page.json new file mode 100644 index 000000000..ca66ac17f --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/ui/layouts/page.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "Heading2-2d1ba9d7-e284-4acd-a37b-e4c8ce45142a", + "type": "Header", + "size": "h2", + "textResourceBindings": { + "title": "Brukeropp-side-overskrift" + } + }, + { + "id": "name", + "type": "Input", + "required": true, + "dataModelBindings": { + "simpleBinding": "melding.name" + } + } + ] + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs index a43627d5d..8510f006e 100644 --- a/test/Altinn.App.Api.Tests/Program.cs +++ b/test/Altinn.App.Api.Tests/Program.cs @@ -1,4 +1,5 @@ using Altinn.App.Api.Extensions; +using Altinn.App.Api.Tests.Data; using Altinn.App.Api.Tests.Mocks; using Altinn.App.Api.Tests.Mocks.Authentication; using Altinn.App.Api.Tests.Mocks.Event; @@ -27,7 +28,9 @@ // external api's etc. should be mocked. WebApplicationBuilder builder = WebApplication.CreateBuilder(new WebApplicationOptions() { ApplicationName = "Altinn.App.Api.Tests" }); +builder.Configuration.AddJsonFile(Path.Join(TestData.GetTestDataRootDirectory(), "apps", "tdd", "contributer-restriction", "appsettings.json")); builder.Configuration.GetSection("MetricsSettings:Enabled").Value = "false"; + ConfigureServices(builder.Services, builder.Configuration); ConfigureMockServices(builder.Services, builder.Configuration); diff --git a/test/Altinn.App.Core.Tests/Features/Validators/Default/DataAnnotationValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/Default/DataAnnotationValidatorTests.cs new file mode 100644 index 000000000..68c8b6bb8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/Default/DataAnnotationValidatorTests.cs @@ -0,0 +1,194 @@ +#nullable enable + +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Validators.Default; + +public class DataAnnotationValidatorTests : IClassFixture +{ + private readonly DataAnnotationValidator _validator; + + public DataAnnotationValidatorTests(DataAnnotationsTestFixture fixture) + { + _validator = fixture.App.Services.GetRequiredKeyedService(DataAnnotationsTestFixture.DataType); + } + + private class TestClass + { + [Required] + [JsonPropertyName("requiredProperty")] + public string? RequiredProperty { get; set; } + + [StringLength(5)] + [JsonPropertyName("stringLength")] + public string? StringLengthProperty { get; set; } + + [Range(1, 10)] + [JsonPropertyName("range")] + public int RangeProperty { get; set; } + + [RegularExpression("^[0-9]*$")] + [JsonPropertyName("regularExpression")] + public string? RegularExpressionProperty { get; set; } + + [EmailAddress] + public string? EmailAddressProperty { get; set; } + + public TestClass? NestedProperty { get; set; } + } + + [Fact] + public async Task ValidateFormData() + { + // Arrange + var instance = new Instance(); + var dataElement = new DataElement(); + var data = new object(); + + // Prepare + + // Act + var result = await _validator.ValidateFormData(instance, dataElement, data); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public async Task Validate_ValidFormData_NoErrors() + { + // Arrange + var instance = new Instance(); + var dataElement = new DataElement(); + var data = new TestClass() + { + RangeProperty = 3, + RequiredProperty = "test", + EmailAddressProperty = "test@altinn.no", + RegularExpressionProperty = "12345", + StringLengthProperty = "12345", + NestedProperty = new TestClass() + { + RangeProperty = 3, + RequiredProperty = "test", + EmailAddressProperty = "test@altinn.no", + RegularExpressionProperty = "12345", + StringLengthProperty = "12345", + } + }; + + // Act + var result = await _validator.ValidateFormData(instance, dataElement, data); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public async Task ValidateFormData_RequiredProperty() + { + // Arrange + var instance = new Instance(); + var dataElement = new DataElement(); + var data = new TestClass() + { + NestedProperty = new(), + }; + + // Act + var result = await _validator.ValidateFormData(instance, dataElement, data); + + // Assert + result.Should().NotBeNull(); + result.Should().BeEquivalentTo(JsonSerializer.Deserialize>(""" + [ + { + "severity": 1, + "instanceId": null, + "dataElementId": null, + "field": "range", + "code": "The field RangeProperty must be between 1 and 10.", + "description": "The field RangeProperty must be between 1 and 10.", + "source": "ModelState", + "customTextKey": null + }, + { + "severity": 1, + "instanceId": null, + "dataElementId": null, + "field": "requiredProperty", + "code": "The RequiredProperty field is required.", + "description": "The RequiredProperty field is required.", + "source": "ModelState", + "customTextKey": null + }, + { + "severity": 1, + "instanceId": null, + "dataElementId": null, + "field": "NestedProperty.range", + "code": "The field RangeProperty must be between 1 and 10.", + "description": "The field RangeProperty must be between 1 and 10.", + "source": "ModelState", + "customTextKey": null + }, + { + "severity": 1, + "instanceId": null, + "dataElementId": null, + "field": "NestedProperty.requiredProperty", + "code": "The RequiredProperty field is required.", + "description": "The RequiredProperty field is required.", + "source": "ModelState", + "customTextKey": null + } + ] + """)); + } +} + +/// +/// System.ComponentModel.DataAnnotations does not provide an easy way to run validations recursively in a unit test, +/// so we need to instantiate a WebApplication to get the IObjectModelValidator. +/// +/// A full WebApplicationFactory seemed a little overkill, so we just use a WebApplicationBuilder. +/// +public class DataAnnotationsTestFixture : IAsyncDisposable +{ + public const string DataType = "test"; + + private readonly DefaultHttpContext _httpContext = new DefaultHttpContext(); + + private readonly Mock _httpContextAccessor = new Mock(); + + public WebApplication App { get; } + + public DataAnnotationsTestFixture() + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.Services.AddMvc(); + builder.Services.AddKeyedTransient(DataType); + _httpContextAccessor.Setup(a => a.HttpContext).Returns(_httpContext); + builder.Services.AddSingleton(_httpContextAccessor.Object); + builder.Services.Configure(builder.Configuration.GetSection("GeneralSettings")); + App = builder.Build(); + } + + public ValueTask DisposeAsync() + { + return App.DisposeAsync(); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/Default/DefaultTaskValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/Default/DefaultTaskValidatorTests.cs new file mode 100644 index 000000000..e3aef2782 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/Default/DefaultTaskValidatorTests.cs @@ -0,0 +1,137 @@ +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Validators.Default; + +public class DefaultTaskValidatorTests +{ + private const string AppId = "tdd/test"; + private const string UnlimitedTaskId = "UnlimitedTask"; + private const string OneRequiredElementTaskId = "OneRequiredElement"; + private const string UnlimitedDataType = "UnlimitedDataId"; + private const string OneRequiredDataType = "OneRequiredDataId"; + + private readonly ApplicationMetadata _applicationMetadata = new(AppId) + { + DataTypes = new List() + { + new() + { + Id = UnlimitedDataType, + TaskId = UnlimitedTaskId, + MaxCount = 0, + MinCount = 0, + }, + new() + { + Id = OneRequiredDataType, + TaskId = OneRequiredElementTaskId, + MinCount = 1, + MaxCount = 1, + } + } + }; + + private readonly Instance _instance = new Instance() + { + Id = $"1234/{Guid.NewGuid()}", + AppId = AppId, + Data = new List(), + }; + + private readonly Mock _appMetadataMock = new(); + private readonly DefaultTaskValidator _sut; + + public DefaultTaskValidatorTests() + { + _appMetadataMock + .Setup(a => a.GetApplicationMetadata()) + .ReturnsAsync(_applicationMetadata); + _sut = new DefaultTaskValidator(_appMetadataMock.Object); + } + + [Fact] + public async Task UnknownTask_NoData_ReturnsNoErrors() + { + var issues = await _sut.ValidateTask(_instance, "unknownTask"); + issues.Should().BeEmpty(); + } + + [Fact] + public async Task UnknownTask_UnknownData_ReturnsNoErrors() + { + _instance.Data.Add(new DataElement + { + DataType = "unknownDataType" + }); + var issues = await _sut.ValidateTask(_instance, "unknownTask"); + issues.Should().BeEmpty(); + } + + [Fact] + public async Task UnlimitedTask_NoData_ReturnsNoErrors() + { + var issues = await _sut.ValidateTask(_instance, UnlimitedTaskId); + issues.Should().BeEmpty(); + } + + [Fact] + public async Task UnlimitedTask_100Data_ReturnsNoErrors() + { + for (var i = 0; i < 100; i++) + { + _instance.Data.Add(new DataElement + { + DataType = UnlimitedDataType + }); + } + + var issues = await _sut.ValidateTask(_instance, UnlimitedTaskId); + issues.Should().BeEmpty(); + } + + [Fact] + public async Task OneRequired_TheOneRequired_ReturnsNoErrors() + { + _instance.Data.Add(new() + { + DataType = OneRequiredDataType + }); + var issues = await _sut.ValidateTask(_instance, OneRequiredElementTaskId); + issues.Should().BeEmpty(); + } + + [Fact] + public async Task OneRequired_NoData_ReturnsError() + { + var issues = await _sut.ValidateTask(_instance, OneRequiredElementTaskId); + var issue = issues.Should().ContainSingle().Which; + issue.Code.Should().Be("TooFewDataElementsOfType"); + issue.Severity.Should().Be(ValidationIssueSeverity.Error); + issue.Field.Should().Be(OneRequiredDataType); + } + + [Fact] + public async Task OneRequired_2Data_ReturnsError() + { + _instance.Data.Add(new() + { + DataType = OneRequiredDataType + }); + _instance.Data.Add(new() + { + DataType = OneRequiredDataType + }); + var issues = await _sut.ValidateTask(_instance, OneRequiredElementTaskId); + var issue = issues.Should().ContainSingle().Which; + issue.Code.Should().Be("TooManyDataElementsOfType"); + issue.Severity.Should().Be(ValidationIssueSeverity.Error); + issue.Field.Should().Be(OneRequiredDataType); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/Default/ExpressionValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/Default/ExpressionValidatorTests.cs new file mode 100644 index 000000000..c4668732f --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/Default/ExpressionValidatorTests.cs @@ -0,0 +1,125 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Models.Layout; +using Altinn.App.Core.Models.Validation; +using Altinn.App.Core.Tests.Helpers; +using Altinn.App.Core.Tests.LayoutExpressions; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; +using Xunit.Sdk; + +namespace Altinn.App.Core.Tests.Features.Validators.Default; + +public class ExpressionValidatorTests +{ + private readonly ExpressionValidator _validator; + private readonly Mock> _logger = new(); + private readonly Mock _appResources = new(MockBehavior.Strict); + private readonly IOptions _frontendSettings = Options.Create(new FrontEndSettings()); + private readonly Mock _layoutInitializer; + + public ExpressionValidatorTests() + { + _layoutInitializer = new(MockBehavior.Strict, _appResources.Object, _frontendSettings) { CallBase = false }; + _validator = + new ExpressionValidator(_logger.Object, _appResources.Object, _layoutInitializer.Object); + } + + [Theory] + [ExpressionTest] + public async Task RunExpressionValidationTest(ExpressionValidationTestModel testCase) + { + var instance = new Instance(); + var dataElement = new DataElement(); + + var dataModel = new JsonDataModel(testCase.FormData); + + var evaluatorState = new LayoutEvaluatorState(dataModel, testCase.Layouts, _frontendSettings.Value, instance); + _layoutInitializer + .Setup(init => init.Init(It.Is(i => i == instance), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(evaluatorState); + _appResources + .Setup(ar => ar.GetValidationConfiguration(null)) + .Returns(JsonSerializer.Serialize(testCase.ValidationConfig)); + + LayoutEvaluator.RemoveHiddenData(evaluatorState, RowRemovalOption.SetToNull); + var validationIssues = await _validator.ValidateFormData(instance, dataElement, null!); + + var result = validationIssues.Select(i => new + { + Message = i.CustomTextKey, + Severity = i.Severity, + Field = i.Field, + }); + + var expected = testCase.Expects.Select(e => new + { + Message = e.Message, + Severity = e.Severity, + Field = e.Field, + }); + + result.Should().BeEquivalentTo(expected); + } +} + +public class ExpressionTestAttribute : DataAttribute +{ + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions + { + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + public override IEnumerable GetData(MethodInfo methodInfo) + { + var files = Directory.GetFiles(Path.Join("Features", "Validators", "shared-expression-validation-tests")); + + foreach (var file in files) + { + var data = File.ReadAllText(file); + ExpressionValidationTestModel testCase = JsonSerializer.Deserialize( + data, + JsonSerializerOptions)!; + yield return new object[] { testCase }; + } + } +} + +public class ExpressionValidationTestModel +{ + public string Name { get; set; } + + public ExpectedObject[] Expects { get; set; } + + public JsonElement ValidationConfig { get; set; } + + public JsonObject FormData { get; set; } + + [JsonConverter(typeof(LayoutModelConverterFromObject))] + public LayoutModel Layouts { get; set; } + + public class ExpectedObject + { + public string Message { get; set; } + + [JsonConverter(typeof(FrontendSeverityConverter))] + public ValidationIssueSeverity Severity { get; set; } + + public string Field { get; set; } + + public string ComponentId { get; set; } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/Default/LegacyIValidationFormDataTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/Default/LegacyIValidationFormDataTests.cs new file mode 100644 index 000000000..c0388866e --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/Default/LegacyIValidationFormDataTests.cs @@ -0,0 +1,138 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Validators.Default +{ + public class LegacyIValidationFormDataTests + { + private readonly LegacyIInstanceValidatorFormDataValidator _validator; + private readonly Mock _instanceValidator = new(); + + public LegacyIValidationFormDataTests() + { + var generalSettings = new GeneralSettings(); + _validator = + new LegacyIInstanceValidatorFormDataValidator(_instanceValidator.Object, Options.Create(generalSettings)); + } + + [Fact] + public async Task ValidateFormData_NoErrors() + { + // Arrange + var data = new object(); + + var validator = new LegacyIInstanceValidatorFormDataValidator(null, Options.Create(new GeneralSettings())); + validator.HasRelevantChanges(data, data).Should().BeFalse(); + + // Act + var result = await validator.ValidateFormData(new Instance(), new DataElement(), data); + + // Assert + Assert.Empty(result); + } + + [Fact] + public async Task ValidateFormData_WithErrors() + { + // Arrange + var data = new object(); + + _instanceValidator + .Setup(iv => iv.ValidateData(It.IsAny(), It.IsAny())) + .Callback((object _, ModelStateDictionary modelState) => + { + modelState.AddModelError("test", "test"); + modelState.AddModelError("ddd", "*FIXED*test"); + }); + + // Act + var result = await _validator.ValidateFormData(new Instance(), new DataElement(), data); + + // Assert + result.Should().BeEquivalentTo( + JsonSerializer.Deserialize>(""" + [ + { + "severity": 4, + "instanceId": null, + "dataElementId": null, + "field": "ddd", + "code": "test", + "description": "test", + "source": "Custom", + "customTextKey": null + }, + { + "severity": 1, + "instanceId": null, + "dataElementId": null, + "field": "test", + "code": "test", + "description": "test", + "source": "Custom", + "customTextKey": null + } + ] + """)); + } + + private class TestModel + { + [JsonPropertyName("test")] + public string Test { get; set; } + + public int IntegerWithout { get; set; } + + [JsonPropertyName("child")] + public TestModel Child { get; set; } + + [JsonPropertyName("children")] + public List TestList { get; set; } + } + + [Theory] + [InlineData("test", "test", "test with small case")] + [InlineData("Test", "test", "test with capital case gets rewritten")] + [InlineData("NotModelMatch", "NotModelMatch", "Error that does not mach model is kept as is")] + [InlineData("Child.TestList[2].child", "child.children[2].child", "TestList is renamed to children because of JsonPropertyName")] + [InlineData("test.children.child", "test.children.child", "valid JsonPropertyName based path is kept as is")] + public async Task ValidateErrorAndMappingWithCustomModel(string errorKey, string field, string errorMessage) + { + // Arrange + var data = new TestModel(); + + _instanceValidator + .Setup(iv => iv.ValidateData(It.IsAny(), It.IsAny())) + .Callback((object _, ModelStateDictionary modelState) => + { + modelState.AddModelError(errorKey, errorMessage); + modelState.AddModelError(errorKey, "*FIXED*" + errorMessage + " Fixed"); + }); + + // Act + var result = await _validator.ValidateFormData(new Instance(), new DataElement(), data); + + // Assert + result.Should().HaveCount(2); + var errorIssue = result.Should().ContainSingle(i => i.Severity == ValidationIssueSeverity.Error).Which; + errorIssue.Field.Should().Be(field); + errorIssue.Severity.Should().Be(ValidationIssueSeverity.Error); + errorIssue.Description.Should().Be(errorMessage); + + var fixedIssue = result.Should().ContainSingle(i => i.Severity == ValidationIssueSeverity.Fixed).Which; + fixedIssue.Field.Should().Be(field); + fixedIssue.Severity.Should().Be(ValidationIssueSeverity.Fixed); + fixedIssue.Description.Should().Be(errorMessage + " Fixed"); + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/ExpressionValidationTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/ExpressionValidationTests.cs index 19d23481d..a8bd5f92c 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/ExpressionValidationTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/ExpressionValidationTests.cs @@ -3,6 +3,7 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Features.Validation.Default; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.Expressions; using Altinn.App.Core.Models.Layout; @@ -23,12 +24,12 @@ public class ExpressionValidationTests [ExpressionTest] public void RunExpressionValidationTest(ExpressionValidationTestModel testCase) { - var logger = Mock.Of>(); + var logger = Mock.Of>(); var dataModel = new JsonDataModel(testCase.FormData); var evaluatorState = new LayoutEvaluatorState(dataModel, testCase.Layouts, new(), new()); LayoutEvaluator.RemoveHiddenData(evaluatorState, RowRemovalOption.SetToNull); - var validationIssues = ExpressionValidator.Validate(testCase.ValidationConfig, dataModel, evaluatorState, logger).ToArray(); + var validationIssues = ExpressionValidator.Validate(testCase.ValidationConfig, evaluatorState, logger).ToArray(); var result = validationIssues.Select(i => new { @@ -52,20 +53,22 @@ public class ExpressionTestAttribute : DataAttribute { public override IEnumerable GetData(MethodInfo methodInfo) { - var files = Directory.GetFiles(Path.Join("Features", "Validators", "shared-expression-validation-tests")); - - foreach (var file in files) - { - var data = File.ReadAllText(file); - ExpressionValidationTestModel testCase = JsonSerializer.Deserialize( - data, - new JsonSerializerOptions + return Directory + .GetFiles(Path.Join("Features", "Validators", "shared-expression-validation-tests")) + .Select(file => + { + var data = File.ReadAllText(file); + return new object[] { - ReadCommentHandling = JsonCommentHandling.Skip, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - })!; - yield return new object[] { testCase }; - } + JsonSerializer.Deserialize( + data, + new JsonSerializerOptions + { + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + })! + }; + }); } } diff --git a/test/Altinn.App.Core.Tests/Features/Validators/GenericValidatorTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/GenericValidatorTests.cs new file mode 100644 index 000000000..687534bb6 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/GenericValidatorTests.cs @@ -0,0 +1,74 @@ +#nullable enable +using System.Text.Json.Serialization; +using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Validators; + +public class GenericValidatorTests +{ + private class MyModel + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("age")] + public int? Age { get; set; } + + [JsonPropertyName("children")] + public List? Children { get; set; } + } + + private class TestValidator : GenericFormDataValidator + { + public TestValidator() : base("MyType") + { + } + + protected override bool HasRelevantChanges(MyModel current, MyModel previous) + { + throw new NotImplementedException(); + } + + protected override Task ValidateFormData(Instance instance, DataElement dataElement, MyModel data) + { + AddValidationIssue(new ValidationIssue() + { + Severity = ValidationIssueSeverity.Informational, + Description = "Test info", + }); + + CreateValidationIssue(c => c.Name, "Test warning", severity: ValidationIssueSeverity.Warning); + var childIndex = 4; + CreateValidationIssue(c => c.Children![childIndex].Children![0].Name, "childrenError", severity: ValidationIssueSeverity.Error); + + return Task.CompletedTask; + } + } + + [Fact] + public async Task VerifyTestValidator() + { + var testValidator = new TestValidator(); + var instance = new Instance(); + var dataElement = new DataElement(); + var data = new MyModel(); + + var validationIssues = await testValidator.ValidateFormData(instance, dataElement, data); + validationIssues.Should().HaveCount(3); + + var info = validationIssues.Should().ContainSingle(c => c.Severity == ValidationIssueSeverity.Informational).Which; + info.Description.Should().Be("Test info"); + + var warning = validationIssues.Should().ContainSingle(c => c.Severity == ValidationIssueSeverity.Warning).Which; + warning.Description.Should().Be("Test warning"); + warning.Field.Should().Be("name"); + + var error = validationIssues.Should().ContainSingle(c => c.Severity == ValidationIssueSeverity.Error).Which; + error.Description.Should().Be("childrenError"); + error.Field.Should().Be("children[4].children[0].name"); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceOldTests.cs similarity index 54% rename from test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs rename to test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceOldTests.cs index 8a275ff2b..9dd8aaada 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/ValidationAppSITests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceOldTests.cs @@ -2,11 +2,14 @@ using System.Text.Json.Serialization; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Features.Validation.Default; +using Altinn.App.Core.Features.Validation.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.Process.Elements; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Enums; @@ -14,27 +17,62 @@ using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace Altinn.App.Core.Tests.Features.Validators; -public class ValidationAppSITests +public class ValidationServiceOldTests { + private readonly Mock> _loggerMock = new(); + private readonly Mock _dataClientMock = new(); + private readonly Mock _appModelMock = new(); + private readonly Mock _appMetadataMock = new(); + private readonly ServiceCollection _serviceCollection = new(); + + private readonly ApplicationMetadata _applicationMetadata = new("tdd/test") + { + DataTypes = new List() + { + new DataType() + { + Id = "test", + TaskId = "Task_1", + EnableFileScan = false, + ValidationErrorOnPendingFileScan = false, + } + } + }; + + public ValidationServiceOldTests() + { + _serviceCollection.AddSingleton(_loggerMock.Object); + _serviceCollection.AddSingleton(_dataClientMock.Object); + _serviceCollection.AddSingleton(); + _serviceCollection.AddSingleton(_appModelMock.Object); + _serviceCollection.AddSingleton(_appMetadataMock.Object); + _serviceCollection.AddSingleton(); + _serviceCollection.AddSingleton(); + _appMetadataMock.Setup(am => am.GetApplicationMetadata()).ReturnsAsync(_applicationMetadata); + } + [Fact] public async Task FileScanEnabled_VirusFound_ValidationShouldFail() { - ValidationAppSI validationAppSI = ConfigureMockServicesForValidation(); + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + IValidationService validationService = serviceProvider.GetRequiredService(); var instance = new Instance(); var dataType = new DataType() { EnableFileScan = true }; var dataElement = new DataElement() { + DataType = "test", FileScanResult = FileScanResult.Infected }; - List validationIssues = await validationAppSI.ValidateDataElement(instance, dataType, dataElement); + List validationIssues = await validationService.ValidateDataElement(instance, dataElement, dataType); validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileInfected").Should().NotBeNull(); } @@ -42,16 +80,21 @@ public async Task FileScanEnabled_VirusFound_ValidationShouldFail() [Fact] public async Task FileScanEnabled_PendingScanNotEnabled_ValidationShouldNotFail() { - ValidationAppSI validationAppSI = ConfigureMockServicesForValidation(); + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + IValidationService validationService = serviceProvider.GetRequiredService(); - var instance = new Instance(); - var dataType = new DataType() { EnableFileScan = true }; + var dataType = new DataType() + { Id = "test", TaskId = "Task_1", AppLogic = null, EnableFileScan = true }; + var instance = new Instance() + { + }; var dataElement = new DataElement() { - FileScanResult = FileScanResult.Pending + DataType = "test", + FileScanResult = FileScanResult.Pending, }; - List validationIssues = await validationAppSI.ValidateDataElement(instance, dataType, dataElement); + List validationIssues = await validationService.ValidateDataElement(instance, dataElement, dataType); validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileScanPending").Should().BeNull(); } @@ -59,16 +102,18 @@ public async Task FileScanEnabled_PendingScanNotEnabled_ValidationShouldNotFail( [Fact] public async Task FileScanEnabled_PendingScanEnabled_ValidationShouldNotFail() { - ValidationAppSI validationAppSI = ConfigureMockServicesForValidation(); + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + IValidationService validationService = serviceProvider.GetRequiredService(); var instance = new Instance(); var dataType = new DataType() { EnableFileScan = true, ValidationErrorOnPendingFileScan = true }; var dataElement = new DataElement() { + DataType = "test", FileScanResult = FileScanResult.Pending }; - List validationIssues = await validationAppSI.ValidateDataElement(instance, dataType, dataElement); + List validationIssues = await validationService.ValidateDataElement(instance, dataElement, dataType); validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileScanPending").Should().NotBeNull(); } @@ -76,16 +121,18 @@ public async Task FileScanEnabled_PendingScanEnabled_ValidationShouldNotFail() [Fact] public async Task FileScanEnabled_Clean_ValidationShouldNotFail() { - ValidationAppSI validationAppSI = ConfigureMockServicesForValidation(); + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + IValidationService validationService = serviceProvider.GetRequiredService(); var instance = new Instance(); var dataType = new DataType() { EnableFileScan = true, ValidationErrorOnPendingFileScan = true }; var dataElement = new DataElement() { - FileScanResult = FileScanResult.Clean + DataType = "test", + FileScanResult = FileScanResult.Clean, }; - List validationIssues = await validationAppSI.ValidateDataElement(instance, dataType, dataElement); + List validationIssues = await validationService.ValidateDataElement(instance, dataElement, dataType); validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileInfected").Should().BeNull(); validationIssues.FirstOrDefault(vi => vi.Code == "DataElementFileScanPending").Should().BeNull(); @@ -95,9 +142,8 @@ public async Task FileScanEnabled_Clean_ValidationShouldNotFail() public async Task ValidateAndUpdateProcess_set_canComplete_validationstatus_and_return_empty_list() { const string taskId = "Task_1"; - + // Mock setup - var appMetadataMock = new Mock(); var appMetadata = new ApplicationMetadata("ttd/test-app") { DataTypes = new List @@ -110,20 +156,22 @@ public async Task ValidateAndUpdateProcess_set_canComplete_validationstatus_and_ } } }; - appMetadataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(appMetadata); - ValidationAppSI validationAppSI = ConfigureMockServicesForValidation(appMetadataMock.Object); - + _appMetadataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(appMetadata); + + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + IValidationService validationService = serviceProvider.GetRequiredService(); + // Testdata var instance = new Instance { - Data = - [ - new DataElement + Data = new List() + { + new() { DataType = "data", ContentType = "application/json" }, - ], + }, Process = new ProcessState { CurrentTask = new ProcessElementInfo @@ -132,20 +180,20 @@ public async Task ValidateAndUpdateProcess_set_canComplete_validationstatus_and_ } } }; - - var issues = await validationAppSI.ValidateAndUpdateProcess(instance, taskId); + + var issues = await validationService.ValidateInstanceAtTask(instance, taskId); issues.Should().BeEmpty(); - instance.Process?.CurrentTask?.Validated.CanCompleteTask.Should().BeTrue(); - instance.Process?.CurrentTask?.Validated.Timestamp.Should().NotBeNull(); + + // instance.Process?.CurrentTask?.Validated.CanCompleteTask.Should().BeTrue(); + // instance.Process?.CurrentTask?.Validated.Timestamp.Should().NotBeNull(); } - + [Fact] public async Task ValidateAndUpdateProcess_set_canComplete_false_validationstatus_and_return_list_of_issues() { const string taskId = "Task_1"; - + // Mock setup - var appMetadataMock = new Mock(); var appMetadata = new ApplicationMetadata("ttd/test-app") { DataTypes = new List @@ -158,27 +206,29 @@ public async Task ValidateAndUpdateProcess_set_canComplete_false_validationstatu } } }; - appMetadataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(appMetadata); - ValidationAppSI validationAppSI = ConfigureMockServicesForValidation(appMetadataMock.Object); - + _appMetadataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(appMetadata); + + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + IValidationService validationService = serviceProvider.GetRequiredService(); + // Testdata var instance = new Instance { - Data = - [ - new DataElement + Data = new List() + { + new() { Id = "3C8B52A9-9602-4B2E-A217-B4E816ED8DEB", DataType = "data", ContentType = "application/json" }, - new DataElement + new() { Id = "3C8B52A9-9602-4B2E-A217-B4E816ED8DEC", DataType = "data", ContentType = "application/json" }, - ], + }, Process = new ProcessState { CurrentTask = new ProcessElementInfo @@ -187,146 +237,116 @@ public async Task ValidateAndUpdateProcess_set_canComplete_false_validationstatu } } }; - - var issues = await validationAppSI.ValidateAndUpdateProcess(instance, taskId); + + var issues = await validationService.ValidateInstanceAtTask(instance, taskId); issues.Should().HaveCount(1); issues.Should().ContainSingle(i => i.Code == ValidationIssueCodes.InstanceCodes.TooManyDataElementsOfType); - instance.Process?.CurrentTask?.Validated.CanCompleteTask.Should().BeFalse(); - instance.Process?.CurrentTask?.Validated.Timestamp.Should().NotBeNull(); - } - private static ValidationAppSI ConfigureMockServicesForValidation(IAppMetadata? appMetadataInput = null, IInstanceValidator? instanceValidatorInput = null) - { - Mock> loggerMock = new(); - var dataMock = new Mock(); - var instanceMock = new Mock(); - var instanceValidator = instanceValidatorInput ?? new Mock().Object; - var appModelMock = new Mock(); - var appResourcesMock = new Mock(); - var appMetadata = appMetadataInput ?? new Mock().Object; - var objectModelValidatorMock = new Mock(); - var layoutEvaluatorStateInitializer = new LayoutEvaluatorStateInitializer(appResourcesMock.Object, Microsoft.Extensions.Options.Options.Create(new Configuration.FrontEndSettings())); - var httpContextAccessorMock = new Mock(); - var generalSettings = Microsoft.Extensions.Options.Options.Create(new Configuration.GeneralSettings()); - var appSettings = Microsoft.Extensions.Options.Options.Create(new Configuration.AppSettings()); - - var validationAppSI = new ValidationAppSI( - loggerMock.Object, - dataMock.Object, - instanceMock.Object, - instanceValidator, - appModelMock.Object, - appResourcesMock.Object, - appMetadata, - objectModelValidatorMock.Object, - layoutEvaluatorStateInitializer, - httpContextAccessorMock.Object, - generalSettings, - appSettings); - return validationAppSI; + // instance.Process?.CurrentTask?.Validated.CanCompleteTask.Should().BeFalse(); + // instance.Process?.CurrentTask?.Validated.Timestamp.Should().NotBeNull(); } [Fact] public void ModelKeyToField_NullInputWithoutType_ReturnsNull() { - ValidationAppSI.ModelKeyToField(null, null!).Should().BeNull(); + ModelStateHelpers.ModelKeyToField(null, null!).Should().BeNull(); } [Fact] public void ModelKeyToField_StringInputWithoutType_ReturnsSameString() { - ValidationAppSI.ModelKeyToField("null", null!).Should().Be("null"); + ModelStateHelpers.ModelKeyToField("null", null!).Should().Be("null"); } [Fact] public void ModelKeyToField_NullInput_ReturnsNull() { - ValidationAppSI.ModelKeyToField(null, typeof(TestModel)).Should().BeNull(); + ModelStateHelpers.ModelKeyToField(null, typeof(TestModel)).Should().BeNull(); } [Fact] public void ModelKeyToField_StringInput_ReturnsSameString() { - ValidationAppSI.ModelKeyToField("null", typeof(TestModel)).Should().Be("null"); + ModelStateHelpers.ModelKeyToField("null", typeof(TestModel)).Should().Be("null"); } [Fact] public void ModelKeyToField_StringInputWithAttr_ReturnsMappedString() { - ValidationAppSI.ModelKeyToField("FirstLevelProp", typeof(TestModel)).Should().Be("level1"); + ModelStateHelpers.ModelKeyToField("FirstLevelProp", typeof(TestModel)).Should().Be("level1"); } [Fact] public void ModelKeyToField_SubModel_ReturnsMappedString() { - ValidationAppSI.ModelKeyToField("SubTestModel.DecimalNumber", typeof(TestModel)).Should().Be("sub.decimal"); + ModelStateHelpers.ModelKeyToField("SubTestModel.DecimalNumber", typeof(TestModel)).Should().Be("sub.decimal"); } [Fact] public void ModelKeyToField_SubModelNullable_ReturnsMappedString() { - ValidationAppSI.ModelKeyToField("SubTestModel.StringNullable", typeof(TestModel)).Should().Be("sub.nullableString"); + ModelStateHelpers.ModelKeyToField("SubTestModel.StringNullable", typeof(TestModel)).Should().Be("sub.nullableString"); } [Fact] public void ModelKeyToField_SubModelWithSubmodel_ReturnsMappedString() { - ValidationAppSI.ModelKeyToField("SubTestModel.StringNullable", typeof(TestModel)).Should().Be("sub.nullableString"); + ModelStateHelpers.ModelKeyToField("SubTestModel.StringNullable", typeof(TestModel)).Should().Be("sub.nullableString"); } [Fact] public void ModelKeyToField_SubModelNull_ReturnsMappedString() { - ValidationAppSI.ModelKeyToField("SubTestModelNull.DecimalNumber", typeof(TestModel)).Should().Be("subnull.decimal"); + ModelStateHelpers.ModelKeyToField("SubTestModelNull.DecimalNumber", typeof(TestModel)).Should().Be("subnull.decimal"); } [Fact] public void ModelKeyToField_SubModelNullNullable_ReturnsMappedString() { - ValidationAppSI.ModelKeyToField("SubTestModelNull.StringNullable", typeof(TestModel)).Should().Be("subnull.nullableString"); + ModelStateHelpers.ModelKeyToField("SubTestModelNull.StringNullable", typeof(TestModel)).Should().Be("subnull.nullableString"); } [Fact] public void ModelKeyToField_SubModelNullWithSubmodel_ReturnsMappedString() { - ValidationAppSI.ModelKeyToField("SubTestModelNull.StringNullable", typeof(TestModel)).Should().Be("subnull.nullableString"); + ModelStateHelpers.ModelKeyToField("SubTestModelNull.StringNullable", typeof(TestModel)).Should().Be("subnull.nullableString"); } // Test lists [Fact] public void ModelKeyToField_List_IgnoresMissingIndex() { - ValidationAppSI.ModelKeyToField("SubTestModelList.StringNullable", typeof(TestModel)).Should().Be("subList.nullableString"); + ModelStateHelpers.ModelKeyToField("SubTestModelList.StringNullable", typeof(TestModel)).Should().Be("subList.nullableString"); } [Fact] public void ModelKeyToField_List_ProxiesIndex() { - ValidationAppSI.ModelKeyToField("SubTestModelList[123].StringNullable", typeof(TestModel)).Should().Be("subList[123].nullableString"); + ModelStateHelpers.ModelKeyToField("SubTestModelList[123].StringNullable", typeof(TestModel)).Should().Be("subList[123].nullableString"); } [Fact] public void ModelKeyToField_ListOfList_ProxiesIndex() { - ValidationAppSI.ModelKeyToField("SubTestModelList[123].ListOfDecimal[5]", typeof(TestModel)).Should().Be("subList[123].decimalList[5]"); + ModelStateHelpers.ModelKeyToField("SubTestModelList[123].ListOfDecimal[5]", typeof(TestModel)).Should().Be("subList[123].decimalList[5]"); } [Fact] public void ModelKeyToField_ListOfList_IgnoresMissing() { - ValidationAppSI.ModelKeyToField("SubTestModelList[123].ListOfDecimal", typeof(TestModel)).Should().Be("subList[123].decimalList"); + ModelStateHelpers.ModelKeyToField("SubTestModelList[123].ListOfDecimal", typeof(TestModel)).Should().Be("subList[123].decimalList"); } [Fact] public void ModelKeyToField_ListOfListNullable_IgnoresMissing() { - ValidationAppSI.ModelKeyToField("SubTestModelList[123].ListOfNullableDecimal", typeof(TestModel)).Should().Be("subList[123].nullableDecimalList"); + ModelStateHelpers.ModelKeyToField("SubTestModelList[123].ListOfNullableDecimal", typeof(TestModel)).Should().Be("subList[123].nullableDecimalList"); } [Fact] public void ModelKeyToField_ListOfListOfListNullable_IgnoresMissingButPropagatesOthers() { - ValidationAppSI.ModelKeyToField("SubTestModelList[123].SubTestModelList.ListOfNullableDecimal[123456]", typeof(TestModel)).Should().Be("subList[123].subList.nullableDecimalList[123456]"); + ModelStateHelpers.ModelKeyToField("SubTestModelList[123].SubTestModelList.ListOfNullableDecimal[123456]", typeof(TestModel)).Should().Be("subList[123].subList.nullableDecimalList[123456]"); } public class TestModel @@ -361,4 +381,4 @@ public class SubTestModel [JsonPropertyName("subList")] public List SubTestModelList { get; set; } = default!; } -} \ No newline at end of file +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs new file mode 100644 index 000000000..d49d5c82c --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Validators/ValidationServiceTests.cs @@ -0,0 +1,116 @@ +#nullable enable +using System.Text.Json.Serialization; +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Validation; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Internal.AppModel; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Models.Validation; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Features.Validators; + +public class ValidationServiceTests +{ + private class MyModel + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("age")] + public int? Age { get; set; } + } + + private static readonly DataElement DefaultDataElement = new() + { + DataType = "MyType", + }; + + private static readonly DataType DefaultDataType = new() + { + Id = "MyType", + }; + + private readonly Mock> _loggerMock = new(); + private readonly Mock _dataClientMock = new(MockBehavior.Strict); + private readonly Mock _appModelMock = new(MockBehavior.Strict); + private readonly Mock _appMetadataMock = new(MockBehavior.Strict); + private readonly Mock _formDataValidatorMock = new(MockBehavior.Strict); + private readonly ServiceCollection _serviceCollection = new(); + + public ValidationServiceTests() + { + _serviceCollection.AddSingleton(_loggerMock.Object); + _serviceCollection.AddSingleton(_dataClientMock.Object); + _serviceCollection.AddSingleton(); + _serviceCollection.AddSingleton(_appModelMock.Object); + _serviceCollection.AddSingleton(_appMetadataMock.Object); + _serviceCollection.AddSingleton(_formDataValidatorMock.Object); + _formDataValidatorMock.Setup(v => v.DataType).Returns(DefaultDataType.Id); + _formDataValidatorMock.Setup(v => v.ValidationSource).Returns("MyNameValidator"); + } + + [Fact] + public async Task ValidateFormData_WithNoValidators_ReturnsNoErrors() + { + _serviceCollection.RemoveAll(typeof(IFormDataValidator)); + + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + + var validatorService = serviceProvider.GetRequiredService(); + var data = new MyModel { Name = "Ola" }; + var result = await validatorService.ValidateFormData(new Instance(), DefaultDataElement, DefaultDataType, data); + result.Should().BeEmpty(); + } + + [Fact] + public async Task ValidateFormData_WithMyNameValidator_ReturnsNoErrorsWhenNameIsOla() + { + _formDataValidatorMock.Setup(v => v.HasRelevantChanges(It.IsAny(), It.IsAny())).Returns(false); + _formDataValidatorMock.Setup(v => v.ValidateFormData(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync((Instance instance, DataElement dataElement, object data) => + { + if (data is MyModel model && model.Name != "Ola") + { + return new List { { new() { Severity = ValidationIssueSeverity.Error, CustomTextKey = "NameNotOla" } } }; + } + + return new List(); + }); + + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + + var validatorService = serviceProvider.GetRequiredService(); + var data = new MyModel { Name = "Ola" }; + var result = await validatorService.ValidateFormData(new Instance(), DefaultDataElement, DefaultDataType, data); + result.Should().ContainKey("MyNameValidator").WhoseValue.Should().HaveCount(0); + result.Should().HaveCount(1); + } + + [Fact] + public async Task ValidateFormData_WithMyNameValidator_ReturnsErrorsWhenNameIsKari() + { + _formDataValidatorMock.Setup(v => v.ValidateFormData(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync((Instance instance, DataElement dataElement, object data) => + { + if (data is MyModel model && model.Name != "Ola") + { + return new List { { new() { Severity = ValidationIssueSeverity.Error, CustomTextKey = "NameNotOla" } } }; + } + + return new List(); + }); + + await using var serviceProvider = _serviceCollection.BuildServiceProvider(); + + var validatorService = serviceProvider.GetRequiredService(); + var data = new MyModel { Name = "Kari" }; + var result = await validatorService.ValidateFormData(new Instance(), DefaultDataElement, DefaultDataType, data); + result.Should().ContainKey("MyNameValidator").WhoseValue.Should().ContainSingle().Which.CustomTextKey.Should().Be("NameNotOla"); + result.Should().HaveCount(1); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs b/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs index 8e40554ad..d4109b727 100644 --- a/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs +++ b/test/Altinn.App.Core.Tests/Helpers/JsonHelperTests.cs @@ -23,12 +23,12 @@ public class JsonHelperTests var logger = new Mock().Object; var guid = Guid.Empty; var dataProcessorMock = new Mock(); - Func?, Task> dataProcessWrite = (instance, guid, model, changes) => Task.FromResult(processDataWriteImpl((TModel)model)); + Func> dataProcessWrite = (instance, guid, model, previousModel) => Task.FromResult(processDataWriteImpl((TModel)model)); dataProcessorMock - .Setup((d) => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>())) + .Setup((d) => d.ProcessDataWrite(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(dataProcessWrite); - return await JsonHelper.ProcessDataWriteWithDiff(instance, guid, model, new IDataProcessor[] { dataProcessorMock.Object }, new(), logger); + return await JsonHelper.ProcessDataWriteWithDiff(instance, guid, model, new IDataProcessor[] { dataProcessorMock.Object }, logger); } public class TestModel diff --git a/test/Altinn.App.Core.Tests/Helpers/LinqExpressionHelpersTests.cs b/test/Altinn.App.Core.Tests/Helpers/LinqExpressionHelpersTests.cs new file mode 100644 index 000000000..cd2152ff2 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/LinqExpressionHelpersTests.cs @@ -0,0 +1,68 @@ +#nullable enable +using System.Text.Json.Serialization; +using Altinn.App.Core.Helpers; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Helpers; + +public class LinqExpressionHelpersTests +{ + public class MyModel + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("age")] + public int? Age { get; set; } + + public List? Children { get; set; } + } + + [Fact] + public void GetJsonPath_OneLevelDeep() + { + var propertyName = LinqExpressionHelpers.GetJsonPath(m => m.Name); + propertyName.Should().Be("name"); + } + + [Fact] + public void GetJsonPath_TwoLevelsDeep() + { + var propertyName = LinqExpressionHelpers.GetJsonPath(m => m.Children![0].Age); + propertyName.Should().Be("Children[0].age"); + } + + [Fact()] + public void GetJsonPath_TwoLevelsDeepUsingFirst() + { + var propertyName = LinqExpressionHelpers.GetJsonPath>(m => m.Children!.Select(c => c.Age)); + propertyName.Should().Be("Children.age"); + } + + [Fact] + public void GetJsonPath_ManyLevelsDeep() + { + var propertyName = LinqExpressionHelpers.GetJsonPath>(m => m.Children![0].Children![2].Children!.Select(c => c.Children![44].Age)); + propertyName.Should().Be("Children[0].Children[2].Children.Children[44].age"); + } + + [Fact] + public void GetJsonPath_IndexInVariable() + { + var index = 123; + var propertyName = LinqExpressionHelpers.GetJsonPath(m => m.Children![index].Age); + propertyName.Should().Be("Children[123].age"); + } + + [Fact] + public void GetJsonPath_IndexInVariableLoop() + { + for (var i = 0; i < 10; i++) + { + var index = i; // Needed to avoid "Access to modified closure" error + var propertyName = LinqExpressionHelpers.GetJsonPath(m => m.Children![index].Age); + propertyName.Should().Be($"Children[{index}].age"); + } + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/ModelDeserializerTests.cs b/test/Altinn.App.Core.Tests/Helpers/ModelDeserializerTests.cs deleted file mode 100644 index 0d775d727..000000000 --- a/test/Altinn.App.Core.Tests/Helpers/ModelDeserializerTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System.Text; -using System.Text.Json.Serialization; -using System.Xml.Serialization; -using Altinn.App.Core.Helpers.Serialization; -using FluentAssertions; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; - -namespace Altinn.App.Core.Tests.Helpers; - -public class ModelDeserializerTests -{ - private readonly ILogger _logger = new Mock().Object; - - [XmlRoot("melding")] - public class Melding - { - [JsonPropertyName("test")] - [XmlElement("test")] - public string Test { get; set; } - } - - [Fact] - public async Task TestDeserializeJson() - { - // Arrange - string json = @"{""test"":""test""}"; - - // Act - var deserializer = new ModelDeserializer(_logger, typeof(Melding)); - var result = await deserializer.DeserializeAsync(new MemoryStream(Encoding.UTF8.GetBytes(json)), "application/json"); - - // Assert - result.HasError.Should().BeFalse(); - result.Model.Should().BeOfType().Which.Test.Should().Be("test"); - } - - [Fact] - public async Task TestDeserializeXml() - { - // Arrange - string json = "test"; - - // Act - var deserializer = new ModelDeserializer(_logger, typeof(Melding)); - var result = await deserializer.DeserializeAsync(new MemoryStream(Encoding.UTF8.GetBytes(json)), "application/xml"); - - // Assert - result.HasError.Should().BeFalse(result.Error); - result.Model.Should().BeOfType().Which.Test.Should().Be("test"); - } - - [Fact] - public async Task TestDeserializeInvalidXml() - { - // Arrange - string json = "test"; - - // Act - var deserializer = new ModelDeserializer(_logger, typeof(Melding)); - var result = await deserializer.DeserializeAsync(new MemoryStream(Encoding.UTF8.GetBytes(json)), "application/xml"); - - // Assert - result.HasError.Should().BeTrue(); - result.Error.Should().Contain("The 'testFail' start tag on line 1 position 11 does not match the end tag of 'estFail'. Line 1, position 26."); - } - - [Fact] - public async Task TestDeserializeMultipartWithInvalidFirstContent() - { - // Arrange - string json = @"{""test"":""test""}"; - using var requestContent = new MultipartFormDataContent(); - requestContent.Add(new StringContent(json, Encoding.UTF8, "application/json"), "ddddd"); - requestContent.Add(new StringContent("invalid", Encoding.UTF8, "application/xml"), "dddd"); - - // Act - var deserializer = new ModelDeserializer(_logger, typeof(Melding)); - - var result = await deserializer.DeserializeAsync(await requestContent.ReadAsStreamAsync(), requestContent.Headers.ContentType!.ToString()); - - // Assert - result.HasError.Should().BeTrue(); - result.Error.Should().Contain("First entry in multipart serialization must have name=\"dataModel\""); - } - - [Fact] - public async Task TestDeserializeMultipartWithInvalidSecondContent() - { - // Arrange - string json = @"{""test"":""test""}"; - using var requestContent = new MultipartFormDataContent(); - requestContent.Add(new StringContent(json, Encoding.UTF8, "application/json"), "dataModel"); - requestContent.Add(new StringContent("invalid", Encoding.UTF8, "application/xml"), "dddd"); - - // Act - var deserializer = new ModelDeserializer(_logger, typeof(Melding)); - - var result = await deserializer.DeserializeAsync(await requestContent.ReadAsStreamAsync(), requestContent.Headers.ContentType!.ToString()); - - // Assert - result.HasError.Should().BeTrue(); - result.Error.Should().Contain("Second entry in multipart serialization must have name=\"previousValues\""); - } - - [Fact] - public async Task TestDeserializeMultipart() - { - // Arrange - string json = @"{""test"":""test""}"; - using var requestContent = new MultipartFormDataContent(); - requestContent.Add(new StringContent(json, Encoding.UTF8, "application/json"), "default"); - requestContent.Add(new StringContent("invalid", Encoding.UTF8, "application/xml"), "dddd"); - - // Act - var deserializer = new ModelDeserializer(_logger, typeof(Melding)); - - var result = await deserializer.DeserializeAsync(await requestContent.ReadAsStreamAsync(), requestContent.Headers.ContentType!.ToString()); - - // Assert - result.HasError.Should().BeTrue(); - result.Error.Should().Contain("First entry in multipart serialization must have name=\"dataModel\""); - } - - [Fact] - public async Task TestDeserializeMultipart_UnknownContentType() - { - // Arrange - string json = @"{""test"":""test""}"; - using var requestContent = new MultipartFormDataContent(); - requestContent.Add(new StringContent(json, Encoding.UTF8, "application/json"), "default"); - requestContent.Add(new StringContent("invalid", Encoding.UTF8, "application/xml"), "dddd"); - - // Act - var deserializer = new ModelDeserializer(_logger, typeof(Melding)); - - var result = await deserializer.DeserializeAsync(await requestContent.ReadAsStreamAsync(), "Unknown Content Type"); - - // Assert - result.HasError.Should().BeTrue(); - result.Error.Should().Contain("Unknown content type Unknown Content Type. Cannot read the data."); - } -} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs b/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs new file mode 100644 index 000000000..1662fe5c8 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/ObjectUtilsTests.cs @@ -0,0 +1,105 @@ +#nullable enable +using Altinn.App.Core.Helpers; +using FluentAssertions; +using Xunit; + +namespace Altinn.App.Core.Tests.Helpers; + +public class ObjectUtilsTests +{ + public class TestClass + { + public string? StringValue { get; set; } + + public decimal Decimal { get; set; } + + public decimal? NullableDecimal { get; set; } + + public TestClass? Child { get; set; } + + public List? Children { get; set; } + } + + [Fact] + public void TestSimple() + { + var test = new TestClass(); + test.Children.Should().BeNull(); + + ObjectUtils.InitializeListsAndNullEmptyStrings(test); + + test.Children.Should().BeEmpty(); + } + + [Fact] + public void TestSimpleStringInitialized() + { + var test = new TestClass() + { + StringValue = "some", + }; + test.Children.Should().BeNull(); + + ObjectUtils.InitializeListsAndNullEmptyStrings(test); + + test.Children.Should().BeEmpty(); + test.StringValue.Should().Be("some"); + } + + [Fact] + public void TestSimpleListInitialized() + { + var test = new TestClass() + { + Children = new(), + }; + test.Children.Should().BeEmpty(); + + ObjectUtils.InitializeListsAndNullEmptyStrings(test); + + test.Children.Should().BeEmpty(); + } + + [Fact] + public void TestMultipleLevelsInitialized() + { + var test = new TestClass() + { + Child = new TestClass() + { + Child = new TestClass() + { + Child = new TestClass() + { + Children = new() + { + new TestClass() + { + Child = new TestClass() + } + } + } + } + } + }; + test.Children.Should().BeNull(); + test.Child.Children.Should().BeNull(); + test.Child.Child.Children.Should().BeNull(); + var subChild = test.Child.Child.Child.Children.Should().ContainSingle().Which; + subChild.Children.Should().BeNull(); + subChild.Child.Should().NotBeNull(); + subChild.Child!.Children.Should().BeNull(); + + // Act + ObjectUtils.InitializeListsAndNullEmptyStrings(test); + + // Assert + test.Children.Should().BeEmpty(); + test.Child.Children.Should().BeEmpty(); + test.Child.Child.Children.Should().BeEmpty(); + subChild = test.Child.Child.Child.Children.Should().ContainSingle().Which; + subChild.Children.Should().BeEmpty(); + subChild.Child.Should().NotBeNull(); + subChild.Child!.Children.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Implementation/NullInstanceValidatorTests.cs b/test/Altinn.App.Core.Tests/Implementation/NullInstanceValidatorTests.cs deleted file mode 100644 index a421a8281..000000000 --- a/test/Altinn.App.Core.Tests/Implementation/NullInstanceValidatorTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Altinn.App.Core.Features.Validation; -using Altinn.App.PlatformServices.Tests.Implementation.TestResources; -using Altinn.Platform.Storage.Interface.Models; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Xunit; - -namespace Altinn.App.PlatformServices.Tests.Implementation; - -public class NullInstanceValidatorTests -{ - [Fact] - public async void NullInstanceValidator_ValidateData_does_not_add_to_ValidationResults() - { - // Arrange - var instanceValidator = new NullInstanceValidator(); - ModelStateDictionary validationResults = new ModelStateDictionary(); - - // Act - await instanceValidator.ValidateData(new DummyModel(), validationResults); - - // Assert - Assert.Empty(validationResults); - } - - [Fact] - public async void NullInstanceValidator_ValidateTask_does_not_add_to_ValidationResults() - { - // Arrange - var instanceValidator = new NullInstanceValidator(); - ModelStateDictionary validationResults = new ModelStateDictionary(); - - // Act - await instanceValidator.ValidateTask(new Instance(), "task0", validationResults); - - // Assert - Assert.Empty(validationResults); - } -} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/SecondPage.json b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/SecondPage.json index 4e6d4a07a..82a10ff86 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/SecondPage.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test2/SecondPage.json @@ -5,8 +5,7 @@ { "id": "firstField", "type": "Summary", - "componentRef": "gruppe1", - "pageRef": "FirstPage" + "componentRef": "gruppe1" } ] } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/SecondPage.json b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/SecondPage.json index 4e6d4a07a..82a10ff86 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/SecondPage.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/Test3/SecondPage.json @@ -5,8 +5,7 @@ { "id": "firstField", "type": "Summary", - "componentRef": "gruppe1", - "pageRef": "FirstPage" + "componentRef": "gruppe1" } ] } From ee15289f3fd94efe37520d4c0d7fc5934152ffa6 Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Wed, 24 Jan 2024 08:22:26 +0100 Subject: [PATCH 45/46] update expression tests (#394) --- .../nested-repeating-hidden-row.json | 6 +- .../nested-repeating-hidden.json | 6 +- .../repeating-hidden-row.json | 3 +- .../repeating-hidden.json | 3 +- .../repeating.json | 3 +- .../context-lists/groups/noData.json | 5 +- .../context-lists/groups/oneRow.json | 3 +- .../context-lists/groups/twoRows.json | 3 +- .../nonRepeatingGroups/maxCount0.json | 96 ------------------- .../nonRepeatingGroups/maxCount1.json | 96 ------------------- .../nonRepeatingGroups/simple.json | 2 +- .../recursiveGroups/recursiveNoData.json | 6 +- .../recursiveGroups/recursiveOneRow.json | 6 +- .../recursiveTwoRowsInner.json | 6 +- .../recursiveTwoRowsOuter.json | 6 +- .../component/across-pages-hidden.json | 9 +- .../functions/component/across-pages.json | 9 +- .../functions/component/duplicate-id-1.json | 9 +- .../functions/component/duplicate-id-2.json | 9 +- .../component/hidden-in-group-other-row.json | 6 +- .../functions/component/hidden-in-group.json | 6 +- .../component/hide-group-component.json | 6 +- .../component/in-group-group-hidden.json | 3 +- .../component/in-group-page-hidden.json | 3 +- .../component/in-group-with-hidden.json | 3 +- .../functions/component/in-group.json | 3 +- .../functions/component/in-nested-group.json | 6 +- .../functions/component/in-nested-group2.json | 6 +- .../dataModel/direct-reference-in-group.json | 3 +- .../direct-reference-in-nested-group.json | 6 +- .../direct-reference-in-nested-group2.json | 6 +- .../direct-reference-in-nested-group3.json | 6 +- .../direct-reference-in-nested-group4.json | 6 +- .../functions/dataModel/in-group.json | 3 +- .../functions/dataModel/in-nested-group.json | 6 +- .../layout-preprocessor/failures.json | 12 +-- .../layout-preprocessor/successful.json | 12 +-- 37 files changed, 66 insertions(+), 322 deletions(-) delete mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount0.json delete mode 100644 test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount1.json diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden-row.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden-row.json index 607327509..04cc5de25 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden-row.json +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden-row.json @@ -140,11 +140,10 @@ "layout": [ { "id": "people-group", - "type": "Group", + "type": "RepeatingGroup", "dataModelBindings": { "group": "form.people" }, - "maxCount": 99, "children": ["number-input", "names-group"], "hiddenRow": ["dataModel", "form.people.hidden"] }, @@ -157,11 +156,10 @@ }, { "id": "names-group", - "type": "Group", + "type": "RepeatingGroup", "dataModelBindings": { "group": "form.people.names" }, - "maxCount": 99, "children": ["name-input"], "hiddenRow": ["dataModel", "form.people.names.hidden"] }, diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden.json index 574a00eed..c5700aefb 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden.json +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/nested-repeating-hidden.json @@ -152,11 +152,10 @@ "layout": [ { "id": "people-group", - "type": "Group", + "type": "RepeatingGroup", "dataModelBindings": { "group": "form.people" }, - "maxCount": 99, "children": ["number-input", "names-group"] }, { @@ -169,11 +168,10 @@ }, { "id": "names-group", - "type": "Group", + "type": "RepeatingGroup", "dataModelBindings": { "group": "form.people.names" }, - "maxCount": 99, "children": ["name-input"] }, { diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden-row.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden-row.json index 65c0b1231..7b1ea3ec7 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden-row.json +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden-row.json @@ -50,11 +50,10 @@ "layout": [ { "id": "names-group", - "type": "Group", + "type": "RepeatingGroup", "dataModelBindings": { "group": "form.names" }, - "maxCount": 99, "children": ["name-input"], "hiddenRow": ["dataModel", "form.names.hidden"] }, diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden.json index 7c5608637..63a9a4260 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden.json +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating-hidden.json @@ -50,11 +50,10 @@ "layout": [ { "id": "names-group", - "type": "Group", + "type": "RepeatingGroup", "dataModelBindings": { "group": "form.names" }, - "maxCount": 99, "children": ["name-input"] }, { diff --git a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating.json b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating.json index 35be8f0e3..8899c0639 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating.json +++ b/test/Altinn.App.Core.Tests/Features/Validators/shared-expression-validation-tests/repeating.json @@ -52,11 +52,10 @@ "layout": [ { "id": "names-group", - "type": "Group", + "type": "RepeatingGroup", "dataModelBindings": { "group": "form.names" }, - "maxCount": 99, "children": ["name-input"] }, { diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/noData.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/noData.json index 1596105a1..551089fc2 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/noData.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/noData.json @@ -10,12 +10,11 @@ }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "children": ["comp3", "comp4"], "dataModelBindings": { "group": "dddd" - }, - "maxCount": 99 + } }, { "id": "comp3", diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/oneRow.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/oneRow.json index b043d9411..5d2852974 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/oneRow.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/oneRow.json @@ -10,11 +10,10 @@ }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "dataModelBindings": { "group": "gruppe1" }, - "maxCount": 99, "children": ["comp3", "comp4"] }, { diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/twoRows.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/twoRows.json index 89dd4f259..0cfecf2d4 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/twoRows.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/groups/twoRows.json @@ -10,11 +10,10 @@ }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "dataModelBindings": { "group": "gruppe1" }, - "maxCount": 99, "children": ["comp3", "comp4"] }, { diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount0.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount0.json deleted file mode 100644 index 8c038be56..000000000 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount0.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "name": "Non-repeating group with maxCount = 0", - "layouts": { - "Page1": { - "data": { - "layout": [ - { - "id": "comp1", - "type": "Header" - }, - { - "id": "group1", - "type": "Group", - "children": ["comp3", "comp4"], - "maxCount": 0 - }, - { - "id": "comp3", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - }, - { - "id": "comp4", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - } - ] - } - }, - "Page2": { - "data": { - "layout": [ - { - "id": "comp5", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - }, - { - "id": "comp6", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - } - ] - } - } - }, - "expectedContexts": [ - { - "component": "Page1", - "currentLayout": "Page1", - "children": [ - { - "component": "comp1", - "currentLayout": "Page1" - }, - { - "component": "group1", - "currentLayout": "Page1", - "children": [ - { - "component": "comp3", - "currentLayout": "Page1" - }, - { - "component": "comp4", - "currentLayout": "Page1" - } - ] - } - ] - }, - { - "component": "Page2", - "currentLayout": "Page2", - "children": [ - { - "component": "comp5", - "currentLayout": "Page2" - }, - { - "component": "comp6", - "currentLayout": "Page2" - } - ] - } - ], - "dataModel": {} -} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount1.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount1.json deleted file mode 100644 index 7019c6eed..000000000 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/maxCount1.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "name": "Non-repeating group with maxCount = 1", - "layouts": { - "Page1": { - "data": { - "layout": [ - { - "id": "comp1", - "type": "Header" - }, - { - "id": "group1", - "type": "Group", - "children": ["comp3", "comp4"], - "maxCount": 1 - }, - { - "id": "comp3", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - }, - { - "id": "comp4", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - } - ] - } - }, - "Page2": { - "data": { - "layout": [ - { - "id": "comp5", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - }, - { - "id": "comp6", - "type": "Input", - "dataModelBindings": { - "simpleBinding": "asdf" - } - } - ] - } - } - }, - "expectedContexts": [ - { - "component": "Page1", - "currentLayout": "Page1", - "children": [ - { - "component": "comp1", - "currentLayout": "Page1" - }, - { - "component": "group1", - "currentLayout": "Page1", - "children": [ - { - "component": "comp3", - "currentLayout": "Page1" - }, - { - "component": "comp4", - "currentLayout": "Page1" - } - ] - } - ] - }, - { - "component": "Page2", - "currentLayout": "Page2", - "children": [ - { - "component": "comp5", - "currentLayout": "Page2" - }, - { - "component": "comp6", - "currentLayout": "Page2" - } - ] - } - ], - "dataModel": {} -} diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/simple.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/simple.json index 56d2c9252..7ea533dd1 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/simple.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/nonRepeatingGroups/simple.json @@ -1,5 +1,5 @@ { - "name": "Non-repeating group with no maxCount", + "name": "Non-repeating group", "layouts": { "Page1": { "data": { diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveNoData.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveNoData.json index 05ff77bc9..13f303325 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveNoData.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveNoData.json @@ -10,18 +10,16 @@ }, { "id": "group0", - "type": "Group", + "type": "RepeatingGroup", "children": ["group1"], - "maxCount": 99, "dataModelBindings": { "group": "group" } }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "children": ["comp3", "comp4"], - "maxCount": 99, "dataModelBindings": { "group": "group.subgroup" } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveOneRow.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveOneRow.json index a0329b437..c51dbdcc2 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveOneRow.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveOneRow.json @@ -10,18 +10,16 @@ }, { "id": "group0", - "type": "Group", + "type": "RepeatingGroup", "children": ["group1"], - "maxCount": 99, "dataModelBindings": { "group": "group" } }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "children": ["comp3", "comp4"], - "maxCount": 99, "dataModelBindings": { "group": "group.subgroup" } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsInner.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsInner.json index d4b60ccd3..19431fc67 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsInner.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsInner.json @@ -10,18 +10,16 @@ }, { "id": "group0", - "type": "Group", + "type": "RepeatingGroup", "children": ["group1"], - "maxCount": 99, "dataModelBindings": { "group": "group" } }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "children": ["comp3", "comp4"], - "maxCount": 99, "dataModelBindings": { "group": "group.subgroup" } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsOuter.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsOuter.json index 0bf29ee1c..847d284f3 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsOuter.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/context-lists/recursiveGroups/recursiveTwoRowsOuter.json @@ -10,18 +10,16 @@ }, { "id": "group0", - "type": "Group", + "type": "RepeatingGroup", "children": ["group1"], - "maxCount": 99, "dataModelBindings": { "group": "group" } }, { "id": "group1", - "type": "Group", + "type": "RepeatingGroup", "children": ["comp3", "comp4"], - "maxCount": 99, "dataModelBindings": { "group": "group.subgroup" } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages-hidden.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages-hidden.json index 92cebf4ce..8f6a573d1 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages-hidden.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages-hidden.json @@ -22,8 +22,7 @@ }, { "id": "page1-Group", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Dyr" }, @@ -52,8 +51,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, @@ -75,8 +73,7 @@ }, { "id": "favoritt-dyr", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker.FavorittDyr" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages.json index 1e13499c2..719ce6824 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/across-pages.json @@ -21,8 +21,7 @@ }, { "id": "page1-Group", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Dyr" }, @@ -51,8 +50,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, @@ -74,8 +72,7 @@ }, { "id": "favoritt-dyr", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker.FavorittDyr" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-1.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-1.json index 0caa9dc70..92d5a2b44 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-1.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-1.json @@ -21,8 +21,7 @@ }, { "id": "page1-Group", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Dyr" }, @@ -51,8 +50,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, @@ -74,8 +72,7 @@ }, { "id": "favoritt-dyr", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker.FavorittDyr" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-2.json index aa6d9f0d3..200a5f4ed 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-2.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/duplicate-id-2.json @@ -21,8 +21,7 @@ }, { "id": "page1-Group", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Dyr" }, @@ -51,8 +50,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, @@ -74,8 +72,7 @@ }, { "id": "favoritt-dyr", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker.FavorittDyr" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group-other-row.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group-other-row.json index dbdee9af8..f615857de 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group-other-row.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group-other-row.json @@ -9,8 +9,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -25,8 +24,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group.json index ec6077e15..60fc6325f 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hidden-in-group.json @@ -9,8 +9,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -25,8 +24,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hide-group-component.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hide-group-component.json index bde26be68..ab8b1ed77 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hide-group-component.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/hide-group-component.json @@ -16,8 +16,7 @@ }, { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -32,8 +31,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-group-hidden.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-group-hidden.json index e08b0eaf9..e7f66b46b 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-group-hidden.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-group-hidden.json @@ -20,8 +20,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-page-hidden.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-page-hidden.json index 953ce3bd0..6a99c1029 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-page-hidden.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-page-hidden.json @@ -21,8 +21,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-with-hidden.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-with-hidden.json index 634a2832c..1662f9e6d 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-with-hidden.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group-with-hidden.json @@ -20,8 +20,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group.json index 3125d5f7c..6a4b79505 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-group.json @@ -20,8 +20,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group.json index 0fd9a9029..e2830ea76 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group2.json index dd1f258fd..d1bc4c89d 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group2.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/in-nested-group2.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-group.json index a6fb981a0..d211b3a24 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-group.json @@ -20,8 +20,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group.json index f411f4af3..c5edf05c6 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group2.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group2.json index 25c2695d7..2b686075b 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group2.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group2.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group3.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group3.json index 3e25751c6..1c3986e5b 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group3.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group3.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group4.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group4.json index e085a88c9..67adb67ad 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group4.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/direct-reference-in-nested-group4.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-group.json index 86c03709e..12e613432 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-group.json @@ -20,8 +20,7 @@ "layout": [ { "id": "myGroup", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Mennesker" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-nested-group.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-nested-group.json index 3ae88bd8b..44b05b56c 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-nested-group.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/in-nested-group.json @@ -20,8 +20,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -36,8 +35,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/failures.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/failures.json index 76925c1bf..52b268015 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/failures.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/failures.json @@ -23,8 +23,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -41,8 +40,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, @@ -103,8 +101,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -121,8 +118,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/successful.json b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/successful.json index a6f15d7ff..415e41a0e 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/successful.json +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/layout-preprocessor/successful.json @@ -23,8 +23,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -41,8 +40,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, @@ -104,8 +102,7 @@ "layout": [ { "id": "bedrifter", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter" }, @@ -122,8 +119,7 @@ }, { "id": "ansatte", - "type": "Group", - "maxCount": 99, + "type": "RepeatingGroup", "dataModelBindings": { "group": "Bedrifter.Ansatte" }, From 07a7897c684132c8c10f136eefab8c06bd9e2485 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Fri, 26 Jan 2024 10:15:02 +0100 Subject: [PATCH 46/46] merge changes from v7 and main into v8 branch. (#404) * chore(deps): update nuget non-major dependencies (#310) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Add [Authorize] attribute on AuthorizationController.GetCurrentParty (#339) This means that the code won't crash with missing cookie when the cookie exipires * chore(deps): update nuget non-major dependencies (#330) * chore(deps): update nuget non-major dependencies * Fix broken test --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Vemund Gaukstad * chore(deps): update dependency moq to v4.20.70 (#371) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update actions/setup-java action to v4 (#372) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update nuget non-major dependencies (#385) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update github/codeql-action action to v3 (#388) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update actions/setup-dotnet action to v4 (#386) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update actions/upload-artifact action to v4 (#387) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Support using connectionString for ApplicationInsights (#379) (#391) This wil become a requirement from may 2025, so adding this early ensures that does not become an requirement for updating apps. https://github.com/microsoft/ApplicationInsights-dotnet/issues/2560 * chore(deps): update nuget non-major dependencies (#390) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * v7: Fix issue with non-ascii characters in code list parameters not being correctly encoded in parameter header. (#402) Likely fix https://github.com/Altinn/codelists-lib-dotnet/issues/23 * Include changes from main in v8 branch mostly verion updates --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Vemund Gaukstad --- .github/workflows/codeql.yml | 6 ++-- .github/workflows/dotnet-test.yml | 2 +- .github/workflows/test-and-analyze-fork.yml | 4 +-- .github/workflows/test-and-analyze.yml | 4 +-- src/Altinn.App.Api/Altinn.App.Api.csproj | 4 +-- .../Controllers/OptionsController.cs | 4 +-- src/Altinn.App.Core/Altinn.App.Core.csproj | 7 +++-- .../Extensions/DictionaryExtensions.cs | 13 ++++++--- .../Altinn.App.Api.Tests.csproj | 8 +++--- .../Controllers/OptionsControllerTests.cs | 4 +-- .../Altinn.App.Common.Tests.csproj | 6 ++-- .../Altinn.App.Core.Tests.csproj | 12 ++++---- .../Extensions/DictionaryExtensionsTests.cs | 28 ++++++++++++++++--- 13 files changed, 64 insertions(+), 38 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6ce02fd3f..81cc7af84 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} queries: security-extended,security-and-quality @@ -54,7 +54,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -67,6 +67,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index e2749d5ac..f25b99e14 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -17,7 +17,7 @@ jobs: DOTNET_HOSTBUILDER__RELOADCONFIGONCHANGE: false steps: - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x diff --git a/.github/workflows/test-and-analyze-fork.yml b/.github/workflows/test-and-analyze-fork.yml index a0c62e349..1fde74299 100644 --- a/.github/workflows/test-and-analyze-fork.yml +++ b/.github/workflows/test-and-analyze-fork.yml @@ -10,7 +10,7 @@ jobs: runs-on: windows-latest steps: - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x @@ -30,7 +30,7 @@ jobs: reportgenerator -reports:TestResults/**/coverage.cobertura.xml -targetdir:TestResults/Output/CoverageReport -reporttypes:Cobertura - name: Archive code coverage results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: code-coverage-report path: TestResults/Output/CoverageReport/ diff --git a/.github/workflows/test-and-analyze.yml b/.github/workflows/test-and-analyze.yml index 37398355c..fb8ae861e 100644 --- a/.github/workflows/test-and-analyze.yml +++ b/.github/workflows/test-and-analyze.yml @@ -16,12 +16,12 @@ jobs: runs-on: windows-latest steps: - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index bebf83d1d..b6378ae28 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -22,11 +22,11 @@ - + - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Altinn.App.Api/Controllers/OptionsController.cs b/src/Altinn.App.Api/Controllers/OptionsController.cs index 1b64ca056..1d6c64057 100644 --- a/src/Altinn.App.Api/Controllers/OptionsController.cs +++ b/src/Altinn.App.Api/Controllers/OptionsController.cs @@ -49,7 +49,7 @@ public async Task Get( return NotFound(); } - HttpContext.Response.Headers.Add("Altinn-DownstreamParameters", appOptions.Parameters.ToNameValueString(',')); + HttpContext.Response.Headers.Append("Altinn-DownstreamParameters", appOptions.Parameters.ToUrlEncodedNameValueString(',')); return Ok(appOptions.Options); } @@ -90,7 +90,7 @@ public async Task Get( return NotFound(); } - HttpContext.Response.Headers.Add("Altinn-DownstreamParameters", appOptions.Parameters.ToNameValueString(',')); + HttpContext.Response.Headers.Append("Altinn-DownstreamParameters", appOptions.Parameters.ToUrlEncodedNameValueString(',')); return Ok(appOptions.Options); } diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index 9393d57d3..d2e10c84f 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -14,11 +14,12 @@ - + - - + + + diff --git a/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs b/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs index 310dbb52a..411cfc5c1 100644 --- a/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs +++ b/src/Altinn.App.Core/Extensions/DictionaryExtensions.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Net; +using System.Text; namespace Altinn.App.Core.Extensions { @@ -8,9 +9,9 @@ namespace Altinn.App.Core.Extensions public static class DictionaryExtensions { /// - /// Converts a dictionary to a name value string on the form key1=value1,key2=value2 + /// Converts a dictionary to a name value string on the form key1=value1,key2=value2 url encoding both key and value. /// - public static string ToNameValueString(this Dictionary parameters, char separator) + public static string ToUrlEncodedNameValueString(this Dictionary? parameters, char separator) { if (parameters == null) { @@ -24,8 +25,12 @@ public static string ToNameValueString(this Dictionary parameter { builder.Append(separator); } - builder.Append(param.Key + "=" + param.Value); + + builder.Append(WebUtility.UrlEncode(param.Key)); + builder.Append('='); + builder.Append(WebUtility.UrlEncode(param.Value)); } + return builder.ToString(); } } diff --git a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj index 2da11ecef..151e0a19b 100644 --- a/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj +++ b/test/Altinn.App.Api.Tests/Altinn.App.Api.Tests.csproj @@ -16,10 +16,10 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs index 2bf455d4a..c5d2222bc 100644 --- a/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/OptionsControllerTests.cs @@ -5,6 +5,7 @@ using Altinn.App.Core.Features; using Altinn.App.Core.Models; using FluentAssertions; +using Moq; namespace Altinn.App.Api.Tests.Controllers { @@ -70,8 +71,7 @@ public Task GetAppOptionsAsync(string language, Dictionary() { { "lang", language } - }, - + } }; return Task.FromResult(appOptions); diff --git a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj index 44eda7035..e0e02bfae 100644 --- a/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj +++ b/test/Altinn.App.Common.Tests/Altinn.App.Common.Tests.csproj @@ -25,9 +25,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj index c6a1d9fc2..02e1881bd 100644 --- a/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj +++ b/test/Altinn.App.Core.Tests/Altinn.App.Core.Tests.csproj @@ -42,11 +42,11 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -57,7 +57,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs b/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs index 4da0bab88..dd7298272 100644 --- a/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs +++ b/test/Altinn.App.Core.Tests/Extensions/DictionaryExtensionsTests.cs @@ -21,7 +21,7 @@ public void ToNameValueString_OptionParameters_ShouldConvertToHttpHeaderFormat() IHeaderDictionary headers = new HeaderDictionary { - { "Altinn-DownstreamParameters", options.Parameters.ToNameValueString(',') } + { "Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',') } }; Assert.Equal("lang=nb,level=1", headers["Altinn-DownstreamParameters"]); @@ -37,7 +37,7 @@ public void ToNameValueString_OptionParametersWithEmptyValue_ShouldConvertToHttp IHeaderDictionary headers = new HeaderDictionary { - { "Altinn-DownstreamParameters", options.Parameters.ToNameValueString(',') } + { "Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',') } }; Assert.Equal(string.Empty, headers["Altinn-DownstreamParameters"]); @@ -48,15 +48,35 @@ public void ToNameValueString_OptionParametersWithNullValue_ShouldConvertToHttpH { var options = new AppOptions { - Parameters = null + Parameters = null! }; IHeaderDictionary headers = new HeaderDictionary { - { "Altinn-DownstreamParameters", options.Parameters.ToNameValueString(',') } + { "Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',') } }; Assert.Equal(string.Empty, headers["Altinn-DownstreamParameters"]); } + + [Fact] + public void ToNameValueString_OptionParametersWithSpecialCharaters_IsValidAsHeaders() + { + var options = new AppOptions + { + Parameters = new Dictionary + { + { "lang", "nb" }, + { "level", "1" }, + { "name", "ÆØÅ" }, + { "variant", "Småvilt1" } + }, + }; + + IHeaderDictionary headers = new HeaderDictionary(); + headers.Add("Altinn-DownstreamParameters", options.Parameters.ToUrlEncodedNameValueString(',')); + + Assert.Equal("lang=nb,level=1,name=%C3%86%C3%98%C3%85,variant=Sm%C3%A5vilt1", headers["Altinn-DownstreamParameters"]); + } } }