From aa2e30128b052c5ecc8afe385162c7d18ef1674a Mon Sep 17 00:00:00 2001 From: Nina Kylstad Date: Wed, 26 Apr 2023 14:37:06 +0200 Subject: [PATCH 1/5] logic for removing shadow fields from form data --- .../Configuration/AppSettings.cs | 9 ++++ .../Helpers/ShadowFieldsConverter.cs | 36 ++++++++++++++ .../Implementation/DefaultTaskEvents.cs | 48 +++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs diff --git a/src/Altinn.App.Core/Configuration/AppSettings.cs b/src/Altinn.App.Core/Configuration/AppSettings.cs index 9974f7960..b4dfbff9a 100644 --- a/src/Altinn.App.Core/Configuration/AppSettings.cs +++ b/src/Altinn.App.Core/Configuration/AppSettings.cs @@ -217,5 +217,14 @@ public string GetResourceFolder() /// Enable the preview functionality to load layout in backend and remove data from hidden components before validation and task completion /// public bool RemoveHiddenDataPreview { get; set; } = false; + + /// + /// Enable the preview functionality to load form data in backend and remove data from shadow field + public string? RemoveShadowFieldsWithPrefix { get; set; } + + /// + /// Specifies data type to store "clean" data without shadow fields in + /// + public string? ShadowFieldCleanDataType { get; set; } } } diff --git a/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs b/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs new file mode 100644 index 000000000..662b01e57 --- /dev/null +++ b/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs @@ -0,0 +1,36 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace Altinn.App.Core.Helpers +{ + public class IgnorePropertiesWithPrefix + { + private readonly string _ignorePrefix; + + public IgnorePropertiesWithPrefix(string prefix) + => _ignorePrefix = prefix; + + public void ModifyPrefixInfo(JsonTypeInfo ti) + { + if (ti.Kind != JsonTypeInfoKind.Object) + return; + + ti.Properties.RemoveAll(prop => prop.Name.StartsWith(_ignorePrefix)); + } + } + + public static class ListHelpers + { + // IList implementation of List.RemoveAll method. + public static void RemoveAll(this IList list, Predicate predicate) + { + for (int i = 0; i < list.Count; i++) + { + if (predicate(list[i])) + { + list.RemoveAt(i--); + } + } + } + } +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs index ca8c905a6..f595ecc6c 100644 --- a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs +++ b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs @@ -1,3 +1,6 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using Altinn.App.Core.Configuration; using Altinn.App.Core.EFormidling.Interface; using Altinn.App.Core.Features; @@ -144,6 +147,8 @@ public async Task OnEndProcessTask(string endEvent, Instance instance) await RunRemoveHiddenData(instance, instanceGuid, dataTypesToLock); + await RunRemoveShadowFields(instance, instanceGuid, dataTypesToLock); + await RunAppDefinedOnTaskEnd(endEvent, instance); await RunLockDataAndGeneratePdf(endEvent, instance, dataTypesToLock); @@ -161,6 +166,13 @@ private async Task RunRemoveHiddenData(Instance instance, Guid instanceGuid, Lis } } + private async Task RunRemoveShadowFields(Instance instance, Guid instanceGuid, List dataTypesToLock) { + if (_appSettings?.RemoveShadowFieldsWithPrefix != null) + { + await RemoveShadowFields(instance, instanceGuid, dataTypesToLock, _appSettings.RemoveShadowFieldsWithPrefix); + } + } + private async Task RunAppDefinedOnTaskEnd(string endEvent, Instance instance) { foreach (var taskEnd in _taskEnds) @@ -256,6 +268,42 @@ private async Task RemoveHiddenData(Instance instance, Guid instanceGuid, List dataTypesToLock, string ignorePrefix) + { + foreach (var dataType in dataTypesToLock.Where(dt => dt.AppLogic != null && dt.Id != _appSettings?.ShadowFieldCleanDataType)) + { + foreach (Guid dataElementId in instance.Data.Where(de => de.DataType == dataType.Id).Select(dataElement => Guid.Parse(dataElement.Id))) + { + Type modelType = _appModel.GetModelType(dataType.AppLogic.ClassRef); + string app = instance.AppId.Split("/")[1]; + int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); + dynamic data = await _dataClient.GetFormData( + instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, dataElementId); + + var modifier = new IgnorePropertiesWithPrefix(ignorePrefix); + JsonSerializerOptions options = new () + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = { modifier.ModifyPrefixInfo } + }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + string serializedData = JsonSerializer.Serialize(data, options); + var updatedData = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedData, modelType); + + if (_appSettings?.ShadowFieldCleanDataType != null) + { + await _dataClient.InsertFormData(updatedData, instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, _appSettings.ShadowFieldCleanDataType); + } + else { + await _dataClient.UpdateData(updatedData, instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, dataElementId); + } + } + } + } + /// public async Task OnAbandonProcessTask(string taskId, Instance instance) { From 14b10568e26318c5c15fa4a0f427e9e41c58acd9 Mon Sep 17 00:00:00 2001 From: Nina Kylstad Date: Wed, 26 Apr 2023 21:38:26 +0200 Subject: [PATCH 2/5] move config to DataType.AppLogic --- .../Configuration/AppSettings.cs | 9 ------- .../Implementation/DefaultTaskEvents.cs | 24 ++++++++++--------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/Altinn.App.Core/Configuration/AppSettings.cs b/src/Altinn.App.Core/Configuration/AppSettings.cs index b4dfbff9a..9974f7960 100644 --- a/src/Altinn.App.Core/Configuration/AppSettings.cs +++ b/src/Altinn.App.Core/Configuration/AppSettings.cs @@ -217,14 +217,5 @@ public string GetResourceFolder() /// Enable the preview functionality to load layout in backend and remove data from hidden components before validation and task completion /// public bool RemoveHiddenDataPreview { get; set; } = false; - - /// - /// Enable the preview functionality to load form data in backend and remove data from shadow field - public string? RemoveShadowFieldsWithPrefix { get; set; } - - /// - /// Specifies data type to store "clean" data without shadow fields in - /// - public string? ShadowFieldCleanDataType { get; set; } } } diff --git a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs index f595ecc6c..76938c31f 100644 --- a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs +++ b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs @@ -167,9 +167,9 @@ private async Task RunRemoveHiddenData(Instance instance, Guid instanceGuid, Lis } private async Task RunRemoveShadowFields(Instance instance, Guid instanceGuid, List dataTypesToLock) { - if (_appSettings?.RemoveShadowFieldsWithPrefix != null) + if (dataTypesToLock.Find(dt => dt.AppLogic?.ShadowFields?.Prefix != null) != null) { - await RemoveShadowFields(instance, instanceGuid, dataTypesToLock, _appSettings.RemoveShadowFieldsWithPrefix); + await RemoveShadowFields(instance, instanceGuid, dataTypesToLock); } } @@ -268,9 +268,9 @@ private async Task RemoveHiddenData(Instance instance, Guid instanceGuid, List dataTypesToLock, string ignorePrefix) + private async Task RemoveShadowFields(Instance instance, Guid instanceGuid, List dataTypesToLock) { - foreach (var dataType in dataTypesToLock.Where(dt => dt.AppLogic != null && dt.Id != _appSettings?.ShadowFieldCleanDataType)) + foreach (var dataType in dataTypesToLock.Where(dt => dt.AppLogic?.ShadowFields != null)) { foreach (Guid dataElementId in instance.Data.Where(de => de.DataType == dataType.Id).Select(dataElement => Guid.Parse(dataElement.Id))) { @@ -280,7 +280,7 @@ private async Task RemoveShadowFields(Instance instance, Guid instanceGuid, List dynamic data = await _dataClient.GetFormData( instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, dataElementId); - var modifier = new IgnorePropertiesWithPrefix(ignorePrefix); + var modifier = new IgnorePropertiesWithPrefix(dataType.AppLogic.ShadowFields.Prefix); JsonSerializerOptions options = new () { TypeInfoResolver = new DefaultJsonTypeInfoResolver @@ -291,13 +291,15 @@ private async Task RemoveShadowFields(Instance instance, Guid instanceGuid, List }; string serializedData = JsonSerializer.Serialize(data, options); - var updatedData = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedData, modelType); - - if (_appSettings?.ShadowFieldCleanDataType != null) - { - await _dataClient.InsertFormData(updatedData, instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, _appSettings.ShadowFieldCleanDataType); + if (dataType.AppLogic.ShadowFields.SaveToDataType != null) { + var saveToDataType = dataTypesToLock.Find(dt => dt.Id == dataType.AppLogic.ShadowFields.SaveToDataType); + Type saveToModelType = _appModel.GetModelType(saveToDataType.AppLogic.ClassRef); + var updatedData = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedData, saveToModelType ?? modelType); + await _dataClient.InsertFormData(updatedData, instanceGuid, saveToModelType ?? modelType, instance.Org, app, instanceOwnerPartyId, saveToDataType.Id); } - else { + else + { + var updatedData = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedData, modelType); await _dataClient.UpdateData(updatedData, instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, dataElementId); } } From 5b6ab7538c8c1e992ffa05b2c6123305f258f4c6 Mon Sep 17 00:00:00 2001 From: Nina Kylstad Date: Mon, 8 May 2023 14:10:19 +0200 Subject: [PATCH 3/5] some cleanup & added tests --- .../Helpers/ShadowFieldsConverter.cs | 16 +- .../Implementation/DefaultTaskEvents.cs | 3 +- .../Helpers/ShadowFieldsConverterTests.cs | 62 +++++ .../Implementation/DefaultTaskEventsTests.cs | 214 ++++++++++++++++++ .../AppDataModel/ModelWithShadowFields.cs | 81 +++++++ 5 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 test/Altinn.App.Core.Tests/Helpers/ShadowFieldsConverterTests.cs create mode 100644 test/Altinn.App.Core.Tests/Implementation/TestData/AppDataModel/ModelWithShadowFields.cs diff --git a/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs b/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs index 662b01e57..13302216e 100644 --- a/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs +++ b/src/Altinn.App.Core/Helpers/ShadowFieldsConverter.cs @@ -3,13 +3,22 @@ namespace Altinn.App.Core.Helpers { + /// + /// This class is used to remove shadow fields from the JSON serialization. + /// public class IgnorePropertiesWithPrefix { private readonly string _ignorePrefix; + /// + /// Initializes a new instance of the class. + /// public IgnorePropertiesWithPrefix(string prefix) => _ignorePrefix = prefix; + /// + /// This method is called by the JSON serializer to remove all properties with the defined prefix. + /// public void ModifyPrefixInfo(JsonTypeInfo ti) { if (ti.Kind != JsonTypeInfoKind.Object) @@ -19,9 +28,14 @@ public void ModifyPrefixInfo(JsonTypeInfo ti) } } + /// + /// This class extends the IList interface with a RemoveAll method. + /// public static class ListHelpers { - // IList implementation of List.RemoveAll method. + /// + /// IList implementation of List.RemoveAll method. + /// public static void RemoveAll(this IList list, Predicate predicate) { for (int i = 0; i < list.Count; i++) diff --git a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs index 76938c31f..c28b79ab7 100644 --- a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs +++ b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs @@ -166,7 +166,8 @@ private async Task RunRemoveHiddenData(Instance instance, Guid instanceGuid, Lis } } - private async Task RunRemoveShadowFields(Instance instance, Guid instanceGuid, List dataTypesToLock) { + private async Task RunRemoveShadowFields(Instance instance, Guid instanceGuid, List dataTypesToLock) + { if (dataTypesToLock.Find(dt => dt.AppLogic?.ShadowFields?.Prefix != null) != null) { await RemoveShadowFields(instance, instanceGuid, dataTypesToLock); diff --git a/test/Altinn.App.Core.Tests/Helpers/ShadowFieldsConverterTests.cs b/test/Altinn.App.Core.Tests/Helpers/ShadowFieldsConverterTests.cs new file mode 100644 index 000000000..2e0c044de --- /dev/null +++ b/test/Altinn.App.Core.Tests/Helpers/ShadowFieldsConverterTests.cs @@ -0,0 +1,62 @@ +#nullable enable +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +using Altinn.App.Core.Helpers; + +using Xunit; + +namespace Altinn.App.PlatformServices.Tests.Helpers; + +public class ShadowFieldsConverterTests +{ + [Fact] + public void ShouldRemoveShadowFields_WithPrefix() + { + var data = new Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields() + { + AltinnSF_hello = "hello", + AltinnSF_test = "test", + Property1 = 1, + Property2 = 2, + AltinnSF_gruppeish = new Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.AltinnSF_gruppeish() + { + F1 = "f1", + F2 = "f2", + }, + Gruppe = new List() + { + new() + { + AltinnSF_gfhjelpefelt = "gfhjelpefelt", + Gf1 = "gf1", + }, + new() + { + AltinnSF_gfhjelpefelt = "gfhjelpefelt2", + Gf1 = "gf1-v2", + } + } + }; + + // Check that regular serialization (without modifier) includes shadow fields in result + string serializedDataWithoutModifier = JsonSerializer.Serialize(data); + Assert.Contains("AltinnSF_", serializedDataWithoutModifier); + + var modifier = new IgnorePropertiesWithPrefix("AltinnSF_"); + JsonSerializerOptions options = new() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = { modifier.ModifyPrefixInfo } + }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + // Check that serialization with modifier removes shadow fields from result + string serializedData = JsonSerializer.Serialize(data, options); + Assert.DoesNotContain("AltinnSF_", serializedData); + } +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs b/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs index 089ada410..38119ba2a 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.Expressions; using Altinn.App.Core.Internal.Pdf; using Altinn.App.Core.Models; +using Altinn.App.Core.Tests.Implementation.TestData.AppDataModel; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -27,6 +28,7 @@ public class DefaultTaskEventsTests: IDisposable private readonly Mock _dataMock; private readonly Mock _prefillMock; private readonly IAppModel _appModel; + private readonly Mock _appModelMock; private readonly Mock _instantiationMock; private readonly Mock _instanceMock; private IEnumerable _taskStarts; @@ -44,6 +46,7 @@ public DefaultTaskEventsTests() _dataMock = new Mock(); _prefillMock = new Mock(); _appModel = new DefaultAppModel(NullLogger.Instance); + _appModelMock = new Mock(); _instantiationMock = new Mock(); _instanceMock = new Mock(); _taskStarts = new List(); @@ -140,6 +143,116 @@ public async void OnEndProcessTask_calls_all_added_implementations_of_IProcessTa endOne.VerifyNoOtherCalls(); endTwo.VerifyNoOtherCalls(); } + + [Fact] + public async void OnEndProcessTask_removes_all_shadow_fields_and_saves_to_specified_datatype() + { + var application = GetApplicationMetadataForShadowFields(); + var instance = new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/shadow-fields-test", + Data = new List() + { + { + new() + { + DataType = "model", + Id = "03ea848c-64f0-40f4-b5b4-30e1642d09b5", + } + } + }, + InstanceOwner = new InstanceOwner() + { + PartyId = "1000" + }, + Org = "ttd" + }; + _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(application); + _appModelMock.Setup(r => r.GetModelType("Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields")).Returns(typeof(Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields)); + var instanceGuid = Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"); + var dataElementId = Guid.Parse("03ea848c-64f0-40f4-b5b4-30e1642d09b5"); + Type modelType = typeof(ModelWithShadowFields); + _dataMock.Setup(r => r.GetFormData(instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)) + .ReturnsAsync(GetDataElementForShadowFields()); + + DefaultTaskEvents te = new DefaultTaskEvents( + _logger, + _resMock.Object, + _metaMock.Object, + _dataMock.Object, + _prefillMock.Object, + _appModelMock.Object, + _instantiationMock.Object, + _instanceMock.Object, + _taskStarts, + _taskEnds, + _taskAbandons, + _pdfMock.Object, + _featureManagerMock.Object, + _layoutStateInitializer); + + await te.OnEndProcessTask("Task_1", instance); + _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])); + } + + [Fact] + public async void OnEndProcessTask_removes_all_shadow_fields_and_saves_to_current_datatype_when_saveToDataType_not_specified() + { + var application = GetApplicationMetadataForShadowFields(false); + var instance = new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/shadow-fields-test", + Data = new List() + { + { + new() + { + DataType = "model", + Id = "03ea848c-64f0-40f4-b5b4-30e1642d09b5", + } + } + }, + InstanceOwner = new InstanceOwner() + { + PartyId = "1000" + }, + Org = "ttd" + }; + _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(application); + _appModelMock.Setup(r => r.GetModelType("Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields")).Returns(typeof(Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields)); + var instanceGuid = Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"); + var dataElementId = Guid.Parse("03ea848c-64f0-40f4-b5b4-30e1642d09b5"); + Type modelType = typeof(Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields); + _dataMock.Setup(r => r.GetFormData(instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)) + .ReturnsAsync(GetDataElementForShadowFields()); + + DefaultTaskEvents te = new DefaultTaskEvents( + _logger, + _resMock.Object, + _metaMock.Object, + _dataMock.Object, + _prefillMock.Object, + _appModelMock.Object, + _instantiationMock.Object, + _instanceMock.Object, + _taskStarts, + _taskEnds, + _taskAbandons, + _pdfMock.Object, + _featureManagerMock.Object, + _layoutStateInitializer); + + await te.OnEndProcessTask("Task_1", instance); + _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])); + } [Fact] public async void OnEndProcessTask_calls_all_added_implementations_of_IProcessTaskStart() @@ -338,4 +451,105 @@ public void Dispose() _instanceMock.VerifyNoOtherCalls(); _pdfMock.VerifyNoOtherCalls(); } + + private ApplicationMetadata GetApplicationMetadataForShadowFields(bool useSaveToDataType = true) + { + return new ApplicationMetadata("tdd/bestilling") + { + Id = "tdd/bestilling", + Org = "tdd", + Created = DateTime.Parse("2019-09-16T22:22:22"), + CreatedBy = "username", + Title = new Dictionary() + { + { "nb", "Bestillingseksempelapp" } + }, + DataTypes = new List() + { + new() + { + Id = "ref-data-as-pdf", + AllowedContentTypes = new List() { "application/pdf" }, + MinCount = 1, + TaskId = "Task_1" + }, + new() + { + Id = "model", + AllowedContentTypes = new List() { "application/xml" }, + MinCount = 1, + MaxCount = 1, + TaskId = "Task_1", + EnablePdfCreation = false, + AppLogic = new ApplicationLogic() + { + AllowAnonymousOnStateless = false, + AutoCreate = true, + ClassRef = "Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields", + AutoDeleteOnProcessEnd = false, + ShadowFields = new ShadowFields() + { + Prefix = "AltinnSF_", + SaveToDataType = useSaveToDataType ? "model-clean" : null, + } + } + }, + new() + { + Id = "model-clean", + AllowedContentTypes = new List() { "application/xml" }, + MinCount = 0, + MaxCount = 1, + TaskId = "Task_1", + AppLogic = new ApplicationLogic() + { + AllowAnonymousOnStateless = false, + AutoCreate = false, + ClassRef = "Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields", + AutoDeleteOnProcessEnd = false, + } + } + }, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true + }, + OnEntry = new OnEntryConfig() + { + Show = "select-instance" + } + }; + } + + private DataElement GetDataElementForShadowFields() + { + return new ModelWithShadowFields() + { + AltinnSF_hello = "hello", + AltinnSF_test = "test", + Property1 = 1, + Property2 = 2, + AltinnSF_gruppeish = new AltinnSF_gruppeish() + { + F1 = "f1", + F2 = "f2", + }, + Gruppe = new List() + { + new() + { + AltinnSF_gfhjelpefelt = "gfhjelpefelt", + Gf1 = "gf1", + }, + new() + { + AltinnSF_gfhjelpefelt = "gfhjelpefelt2", + Gf1 = "gf1-v2", + } + } + }; + } } \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Implementation/TestData/AppDataModel/ModelWithShadowFields.cs b/test/Altinn.App.Core.Tests/Implementation/TestData/AppDataModel/ModelWithShadowFields.cs new file mode 100644 index 000000000..4c4c0853a --- /dev/null +++ b/test/Altinn.App.Core.Tests/Implementation/TestData/AppDataModel/ModelWithShadowFields.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Newtonsoft.Json; + +namespace Altinn.App.Core.Tests.Implementation.TestData.AppDataModel +{ + [XmlRoot(ElementName="model")] + public class ModelWithShadowFields + { + [Range(double.MinValue, double.MaxValue)] + [XmlElement("property1", Order = 1)] + [JsonProperty("property1")] + [JsonPropertyName("property1")] + [Required] + public decimal? Property1 { get; set; } + + [Range(double.MinValue, double.MaxValue)] + [XmlElement("property2", Order = 2)] + [JsonProperty("property2")] + [JsonPropertyName("property2")] + [Required] + public decimal? Property2 { get; set; } + + [XmlElement("property3", Order = 3)] + [JsonProperty("property3")] + [JsonPropertyName("property3")] + public string Property3 { get; set; } + + [XmlElement("AltinnSF_hello", Order = 4)] + [JsonProperty("AltinnSF_hello")] + [JsonPropertyName("AltinnSF_hello")] + public string AltinnSF_hello { get; set; } + + [XmlElement("AltinnSF_test", Order = 5)] + [JsonProperty("AltinnSF_test")] + [JsonPropertyName("AltinnSF_test")] + public string AltinnSF_test { get; set; } + + [XmlElement("AltinnSF_gruppeish", Order = 6)] + [JsonProperty("AltinnSF_gruppeish")] + [JsonPropertyName("AltinnSF_gruppeish")] + public AltinnSF_gruppeish AltinnSF_gruppeish { get; set; } + + [XmlElement("gruppe", Order = 7)] + [JsonProperty("gruppe")] + [JsonPropertyName("gruppe")] + public List Gruppe { get; set; } + } + + public class AltinnSF_gruppeish + { + [XmlElement("f1", Order = 1)] + [JsonProperty("f1")] + [JsonPropertyName("f1")] + public string F1 { get; set; } + + [XmlElement("f2", Order = 2)] + [JsonProperty("f2")] + [JsonPropertyName("f2")] + public string F2 { get; set; } + } + + public class Gruppe + { + [XmlElement("gf1", Order = 1)] + [JsonProperty("gf1")] + [JsonPropertyName("gf1")] + public string Gf1 { get; set; } + + [XmlElement("AltinnSF_gf-hjelpefelt", Order = 2)] + [JsonProperty("AltinnSF_gf-hjelpefelt")] + [JsonPropertyName("AltinnSF_gf-hjelpefelt")] + public string AltinnSF_gfhjelpefelt { get; set; } + } +} From 45fdb481678650e4dd5b838d761d3fa95aa701b7 Mon Sep 17 00:00:00 2001 From: Nina Kylstad Date: Fri, 12 May 2023 11:07:34 +0200 Subject: [PATCH 4/5] use system.text.json instead of newtonsoft --- .../Implementation/DefaultTaskEvents.cs | 8 ++- .../Implementation/DefaultTaskEventsTests.cs | 59 ++++++++++++++++++- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs index c28b79ab7..cc55b5c4f 100644 --- a/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs +++ b/src/Altinn.App.Core/Implementation/DefaultTaskEvents.cs @@ -294,13 +294,17 @@ private async Task RemoveShadowFields(Instance instance, Guid instanceGuid, List string serializedData = JsonSerializer.Serialize(data, options); if (dataType.AppLogic.ShadowFields.SaveToDataType != null) { var saveToDataType = dataTypesToLock.Find(dt => dt.Id == dataType.AppLogic.ShadowFields.SaveToDataType); + if (saveToDataType == null) { + throw new Exception($"SaveToDataType {dataType.AppLogic.ShadowFields.SaveToDataType} not found"); + } + Type saveToModelType = _appModel.GetModelType(saveToDataType.AppLogic.ClassRef); - var updatedData = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedData, saveToModelType ?? modelType); + var updatedData = JsonSerializer.Deserialize(serializedData, saveToModelType); await _dataClient.InsertFormData(updatedData, instanceGuid, saveToModelType ?? modelType, instance.Org, app, instanceOwnerPartyId, saveToDataType.Id); } else { - var updatedData = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedData, modelType); + var updatedData = JsonSerializer.Deserialize(serializedData, modelType); await _dataClient.UpdateData(updatedData, instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, dataElementId); } } diff --git a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs b/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs index 38119ba2a..db5e0d72f 100644 --- a/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/DefaultTaskEventsTests.cs @@ -253,6 +253,59 @@ public async void OnEndProcessTask_removes_all_shadow_fields_and_saves_to_curren _dataMock.Verify(r => r.GetFormData(instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)); _dataMock.Verify(r => r.Update(instance, instance.Data[0])); } + + [Fact] + public async void OnEndProcessTask_throws_exception_when_saveToDataType_is_specified_but_does_not_exist() + { + var application = GetApplicationMetadataForShadowFields(true, saveToDataType: "does-not-exist"); + var instance = new Instance() + { + Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", + AppId = "ttd/shadow-fields-test", + Data = new List() + { + { + new() + { + DataType = "model", + Id = "03ea848c-64f0-40f4-b5b4-30e1642d09b5", + } + } + }, + InstanceOwner = new InstanceOwner() + { + PartyId = "1000" + }, + Org = "ttd" + }; + _metaMock.Setup(r => r.GetApplicationMetadata()).ReturnsAsync(application); + _appModelMock.Setup(r => r.GetModelType("Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields")).Returns(typeof(Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields)); + var instanceGuid = Guid.Parse("fa0678ad-960d-4307-aba2-ba29c9804c9d"); + var dataElementId = Guid.Parse("03ea848c-64f0-40f4-b5b4-30e1642d09b5"); + Type modelType = typeof(Altinn.App.Core.Tests.Implementation.TestData.AppDataModel.ModelWithShadowFields); + _dataMock.Setup(r => r.GetFormData(instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)) + .ReturnsAsync(GetDataElementForShadowFields()); + + DefaultTaskEvents te = new DefaultTaskEvents( + _logger, + _resMock.Object, + _metaMock.Object, + _dataMock.Object, + _prefillMock.Object, + _appModelMock.Object, + _instantiationMock.Object, + _instanceMock.Object, + _taskStarts, + _taskEnds, + _taskAbandons, + _pdfMock.Object, + _featureManagerMock.Object, + _layoutStateInitializer); + + await Assert.ThrowsAsync(async () => await te.OnEndProcessTask("Task_1", instance)); + _metaMock.Verify(r => r.GetApplicationMetadata()); + _dataMock.Verify(r => r.GetFormData(instanceGuid, modelType, "ttd", "shadow-fields-test", 1000, dataElementId)); + } [Fact] public async void OnEndProcessTask_calls_all_added_implementations_of_IProcessTaskStart() @@ -452,7 +505,7 @@ public void Dispose() _pdfMock.VerifyNoOtherCalls(); } - private ApplicationMetadata GetApplicationMetadataForShadowFields(bool useSaveToDataType = true) + private ApplicationMetadata GetApplicationMetadataForShadowFields(bool useSaveToDataType = true, string saveToDataType = "model-clean") { return new ApplicationMetadata("tdd/bestilling") { @@ -490,7 +543,7 @@ private ApplicationMetadata GetApplicationMetadataForShadowFields(bool useSaveTo ShadowFields = new ShadowFields() { Prefix = "AltinnSF_", - SaveToDataType = useSaveToDataType ? "model-clean" : null, + SaveToDataType = useSaveToDataType ? saveToDataType : null, } } }, @@ -524,7 +577,7 @@ private ApplicationMetadata GetApplicationMetadataForShadowFields(bool useSaveTo }; } - private DataElement GetDataElementForShadowFields() + private ModelWithShadowFields GetDataElementForShadowFields() { return new ModelWithShadowFields() { From ff9315fe056a4b940965cd74da1cd2aba6d41f9f Mon Sep 17 00:00:00 2001 From: Nina Kylstad Date: Fri, 12 May 2023 20:09:11 +0200 Subject: [PATCH 5/5] update Storage.Interface.Models to v3.17.0 --- src/Altinn.App.Api/Altinn.App.Api.csproj | 2 +- src/Altinn.App.Core/Altinn.App.Core.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index f6e123ad5..3bf535892 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index ab1d3beb7..181606f7e 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -13,7 +13,7 @@ - +