From 05e4e437a871f1edbca510c69a6c705d06b78c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rnar=20Prytz?= Date: Fri, 19 Sep 2025 09:38:11 +0200 Subject: [PATCH 1/2] de/serailize json --- .../ModelSerializationService.cs | 36 ++- .../Internal/Data/InstanceDataUnitOfWork.cs | 6 +- .../Mocks/DataClientMock.cs | 2 +- .../Models/ModelSerializationServiceTests.cs | 213 ++++++++++++++++++ ...ouldNotChange_Unintentionally.verified.txt | 2 +- 5 files changed, 249 insertions(+), 10 deletions(-) create mode 100644 test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs index 1bc10a767..88605478c 100644 --- a/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs +++ b/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs @@ -37,13 +37,31 @@ public ModelSerializationService(IAppModel appModel, Telemetry? telemetry = null /// /// The binary data /// The data type used to get content type and the classRef for the object to be returned + /// /// The model specified in - public object DeserializeFromStorage(ReadOnlySpan data, DataType dataType) + public object DeserializeFromStorage( + ReadOnlySpan data, + DataType dataType, + string? contentType = "application/xml" + ) { + contentType ??= "application/xml"; + + if (dataType.AllowedContentTypes?.Contains(contentType) == false) + { + throw new InvalidOperationException( + $"Content type {contentType} is not allowed for data type {dataType.Id}" + ); + } + var type = GetModelTypeForDataType(dataType); - // TODO: support sending json to storage based on dataType.ContentTypes - return DeserializeXml(data, type); + return contentType switch + { + "application/xml" => DeserializeXml(data, type), + "application/json" => DeserializeJson(data, type), + _ => throw new InvalidOperationException($"Unsupported content type {contentType}"), + }; } /// @@ -51,7 +69,7 @@ public object DeserializeFromStorage(ReadOnlySpan data, DataType dataType) /// /// The object to serialize (must match the classRef in DataType) /// The data type - /// the binary data and the content type (currently only application/xml, but likely also json in the future) + /// the binary data and the content type (application/xml or application/json) /// If the classRef in dataType does not match type of the model public (ReadOnlyMemory data, string contentType) SerializeToStorage(object model, DataType dataType) { @@ -63,8 +81,14 @@ public object DeserializeFromStorage(ReadOnlySpan data, DataType dataType) ); } - //TODO: support sending json to storage based on dataType.ContentTypes - return (SerializeToXml(model), "application/xml"); + var contentType = dataType.AllowedContentTypes.FirstOrDefault() ?? "application/xml"; + + return contentType switch + { + "application/xml" => (SerializeToXml(model), contentType), + "application/json" => (SerializeToJson(model), contentType), + _ => throw new InvalidOperationException($"Unsupported content type {contentType}"), + }; } /// diff --git a/src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs b/src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs index 2d6503da8..76b9d0dff 100644 --- a/src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs +++ b/src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs @@ -103,7 +103,8 @@ public async Task GetFormData(DataElementIdentifier dataElementIdentifie return _modelSerializationService.DeserializeFromStorage( binaryData.Span, - this.GetDataType(dataElementIdentifier) + this.GetDataType(dataElementIdentifier), + GetDataElement(dataElementIdentifier).ContentType ); } ); @@ -359,7 +360,8 @@ out ReadOnlyMemory cachedBinary // and deserializing twice is not a big deal PreviousFormData = _modelSerializationService.DeserializeFromStorage( cachedBinary.Span, - dataType + dataType, + dataElement.ContentType ), CurrentBinaryData = currentBinary, PreviousBinaryData = cachedBinary, diff --git a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs index aaf837853..da01bb3a3 100644 --- a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs @@ -204,7 +204,7 @@ await File.ReadAllBytesAsync(dataElementPath, cancellationToken), string dataPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataId); var dataBytes = await File.ReadAllBytesAsync(dataPath, cancellationToken); - var formData = _modelSerialization.DeserializeFromStorage(dataBytes, dataType); + var formData = _modelSerialization.DeserializeFromStorage(dataBytes, dataType, dataElement?.ContentType); return formData ?? throw new Exception("Unable to deserialize form data"); } diff --git a/test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs b/test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs new file mode 100644 index 000000000..3f96c8648 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs @@ -0,0 +1,213 @@ +using Altinn.App.Core.Helpers.Serialization; +using Altinn.App.Core.Internal.AppModel; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Moq; + +namespace Altinn.App.Core.Tests.Models; + +public class ModelSerializationServiceTests +{ + private static readonly string _testClassRef = typeof(TestDataModel).AssemblyQualifiedName!; + private static readonly string _otherClassRef = typeof(SomeOtherType).AssemblyQualifiedName!; + + private readonly ModelSerializationService _sut; + + private readonly Mock _appModelMock = new Mock(); + + public ModelSerializationServiceTests() + { + _appModelMock.Setup(x => x.GetModelType(It.Is(s => s == _testClassRef))).Returns(typeof(TestDataModel)); + _appModelMock + .Setup(x => x.GetModelType(It.Is(s => s == _otherClassRef))) + .Returns(typeof(SomeOtherType)); + + _sut = new ModelSerializationService(_appModelMock.Object); + } + + [Theory] + [InlineData("application/json", "application/json")] + [InlineData("application/xml", "application/xml")] + [InlineData(null, "application/xml")] + public void SerializeToStorage_ReturnsExpectedContentType(string? contentType, string expectedOutputType) + { + List allowedContentTypes = contentType != null ? [contentType] : []; + + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + + var dataType = CreateDataType(allowedContentTypes); + + // Act + var (_, outputContentType) = _sut.SerializeToStorage(testObject, dataType); + + // Assert + outputContentType.Should().Be(expectedOutputType); + } + + [Fact] + public void SerializeToStorage_SerializesJson() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + + var dataType = CreateDataType(["application/json"]); + + // Act + var (data, _) = _sut.SerializeToStorage(testObject, dataType); + + // Assert + var json = System.Text.Encoding.UTF8.GetString(data.Span); + + json.Should().Be("{\"name\":\"Test\",\"value\":42}"); + } + + [Fact] + public void SerializeToStorage_SerializesXml() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + + var dataType = CreateDataType(["application/xml"]); + + // Act + var (data, _) = _sut.SerializeToStorage(testObject, dataType); + + // Assert + var xml = System.Text.Encoding.UTF8.GetString(data.Span); + + xml.Should() + .Be( + "Test42" + ); + } + + [Fact] + public void SerializeToStorage_ThrowsOnUnsupportedContentType() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + + var dataType = CreateDataType(["application/unsupported"]); + + // Act + var act = () => _sut.SerializeToStorage(testObject, dataType); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void SerializeToStorage_ThrowsOnMismatchingModelType() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + + var mismatchingDataType = new DataType() + { + Id = "mismatching", + AppLogic = new ApplicationLogic() { ClassRef = _otherClassRef }, + AllowedContentTypes = ["application/json"], + }; + + // Act + var act = () => _sut.SerializeToStorage(testObject, mismatchingDataType); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void DeserializeFromStorage_ThrowsOnMismatchingModelType() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + var data = _sut.SerializeToJson(testObject); + + var mismatchingDataType = new DataType() + { + Id = "mismatching", + AppLogic = new ApplicationLogic() { ClassRef = _otherClassRef }, + }; + + // Act + var act = () => _sut.DeserializeFromStorage(data.Span, mismatchingDataType); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void DeserializeFromStorage_DeserializesJson() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + var data = _sut.SerializeToJson(testObject); + + var dataType = CreateDataType(["application/json"]); + + // Act + var result = _sut.DeserializeFromStorage(data.Span, dataType, "application/json"); + + // Assert + result.Should().BeOfType(); + var model = (TestDataModel)result; + model.Name.Should().Be("Test"); + model.Value.Should().Be(42); + } + + [Fact] + public void DeserializeFromStorage_DeserializesXml() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + var data = _sut.SerializeToXml(testObject); + + var dataType = CreateDataType(["application/xml"]); + + // Act + var result = _sut.DeserializeFromStorage(data.Span, dataType, "application/xml"); + + // Assert + result.Should().BeOfType(); + var model = (TestDataModel)result; + model.Name.Should().Be("Test"); + model.Value.Should().Be(42); + } + + [Fact] + public void DeserializeFromStorage_WithDefaultContentType_DeserializesXml() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + var data = _sut.SerializeToXml(testObject); + + var dataType = CreateDataType(["application/xml"]); + + // Act + var result = _sut.DeserializeFromStorage(data.Span, dataType); + + // Assert + result.Should().BeOfType(); + var model = (TestDataModel)result; + model.Name.Should().Be("Test"); + model.Value.Should().Be(42); + } + + private static DataType CreateDataType(List allowedContentTypes) + { + return new DataType() + { + AppLogic = new ApplicationLogic() { ClassRef = _testClassRef }, + AllowedContentTypes = allowedContentTypes, + }; + } + + public record SomeOtherType { } + + public record TestDataModel + { + public string Name { get; set; } = string.Empty; + public int Value { get; set; } + } +} diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 8e71e4f24..45e3ddbaf 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -2229,7 +2229,7 @@ namespace Altinn.App.Core.Helpers.Serialization public class ModelSerializationService { public ModelSerializationService(Altinn.App.Core.Internal.AppModel.IAppModel appModel, Altinn.App.Core.Features.Telemetry? telemetry = null) { } - public object DeserializeFromStorage(System.ReadOnlySpan data, Altinn.Platform.Storage.Interface.Models.DataType dataType) { } + public object DeserializeFromStorage(System.ReadOnlySpan data, Altinn.Platform.Storage.Interface.Models.DataType dataType, string? contentType = "application/xml") { } public object DeserializeJson(System.ReadOnlySpan data, System.Type modelType) { } public System.Threading.Tasks.Task> DeserializeSingleFromStream(System.IO.Stream body, string? contentType, Altinn.Platform.Storage.Interface.Models.DataType dataType) { } public object DeserializeXml(System.ReadOnlySpan data, System.Type modelType) { } From c7bfa313635c0960b67d39fc490ef3dcca2918e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rnar=20Prytz?= Date: Fri, 19 Sep 2025 12:20:58 +0200 Subject: [PATCH 2/2] Add one more test, and fix the failing ones --- .../Serialization/ModelSerializationService.cs | 2 +- .../Models/ModelSerializationServiceTests.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs index 88605478c..bb9189805 100644 --- a/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs +++ b/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs @@ -81,7 +81,7 @@ public object DeserializeFromStorage( ); } - var contentType = dataType.AllowedContentTypes.FirstOrDefault() ?? "application/xml"; + var contentType = dataType.AllowedContentTypes?.FirstOrDefault() ?? "application/xml"; return contentType switch { diff --git a/test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs b/test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs index 3f96c8648..2d88793fc 100644 --- a/test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs @@ -117,6 +117,21 @@ public void SerializeToStorage_ThrowsOnMismatchingModelType() act.Should().Throw(); } + [Fact] + public void SerializeToStorage_MultipleAllowedContentTypes_PicksTheFirstOne() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + + var dataType = CreateDataType(["application/json", "application/xml"]); + + // Act + var (_, outputContentType) = _sut.SerializeToStorage(testObject, dataType); + + // Assert + outputContentType.Should().Be("application/json"); + } + [Fact] public void DeserializeFromStorage_ThrowsOnMismatchingModelType() {