diff --git a/.editorconfig b/.editorconfig index 251ac0ff4..2aa35c177 100644 --- a/.editorconfig +++ b/.editorconfig @@ -135,6 +135,9 @@ dotnet_diagnostic.CA2254.severity = none # CA1822: Mark members as static dotnet_diagnostic.CA1822.severity = warning +# CA1725: Parameter names should match base declaration +dotnet_diagnostic.CA1725.severity = warning + # IDE0080: Remove unnecessary suppression operator dotnet_diagnostic.IDE0080.severity = error diff --git a/src/Altinn.App.Analyzers/FormDataWrapper/FormDataWrapperAnalyzer.cs b/src/Altinn.App.Analyzers/FormDataWrapper/FormDataWrapperAnalyzer.cs index 098661e19..14e4284a9 100644 --- a/src/Altinn.App.Analyzers/FormDataWrapper/FormDataWrapperAnalyzer.cs +++ b/src/Altinn.App.Analyzers/FormDataWrapper/FormDataWrapperAnalyzer.cs @@ -6,12 +6,12 @@ public class FormDataWrapperAnalyzer : DiagnosticAnalyzer public override ImmutableArray SupportedDiagnostics => [Diagnostics.FormDataWrapperGenerator.AppMetadataError]; - public override void Initialize(AnalysisContext analysisContext) + public override void Initialize(AnalysisContext context) { - analysisContext.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - analysisContext.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); - analysisContext.RegisterCompilationAction(CompilationAnalysisAction); + context.RegisterCompilationAction(CompilationAnalysisAction); } private void CompilationAnalysisAction(CompilationAnalysisContext compilationContext) diff --git a/src/Altinn.App.Analyzers/Utils/EquatableArray.cs b/src/Altinn.App.Analyzers/Utils/EquatableArray.cs index 7c8cef0ec..903643903 100644 --- a/src/Altinn.App.Analyzers/Utils/EquatableArray.cs +++ b/src/Altinn.App.Analyzers/Utils/EquatableArray.cs @@ -30,18 +30,18 @@ public EquatableArray(T[]? array) } /// - public bool Equals(EquatableArray array) + public bool Equals(EquatableArray other) { - return AsSpan().SequenceEqual(array.AsSpan()); + return AsSpan().SequenceEqual(other.AsSpan()); } - /// + /// public override bool Equals(object? obj) { return obj is EquatableArray array && this.Equals(array); } - /// + /// public override int GetHashCode() { if (_array is null) diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index 18ae72a25..05d3eb2d6 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -535,19 +535,17 @@ public async Task Get( { return await GetFormData( org, - app, instanceOwnerPartyId, instanceGuid, instance, dataGuid, dataElement, - dataTypeObject, includeRowId, language ); } - return await GetBinaryData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, dataElement); + return await GetBinaryData(org, instanceOwnerPartyId, instanceGuid, dataGuid, dataElement); } catch (PlatformHttpException e) { @@ -898,14 +896,13 @@ private ObjectResult ExceptionResponse(Exception exception, string message) /// The data element is returned in the body of the response private async Task GetBinaryData( string org, - string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid, DataElement dataElement ) { - Stream dataStream = await _dataClient.GetBinaryData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid); + Stream dataStream = await _dataClient.GetBinaryData(instanceOwnerPartyId, instanceGuid, dataGuid); if (dataStream is not null) { @@ -935,26 +932,17 @@ DataElement dataElement /// data element is returned in response body private async Task GetFormData( string org, - string app, int instanceOwnerId, Guid instanceGuid, Instance instance, Guid dataGuid, DataElement dataElement, - DataType dataType, bool includeRowId, string? language ) { // Get Form Data from data service. Assumes that the data element is form data. - object appModel = await _dataClient.GetFormData( - instanceGuid, - _appModel.GetModelType(dataType.AppLogic.ClassRef), - org, - app, - instanceOwnerId, - dataGuid - ); + object appModel = await _dataClient.GetFormData(instance, dataElement); if (appModel is null) { @@ -985,15 +973,7 @@ private async Task GetFormData( { try { - await _dataClient.UpdateData( - appModel, - instanceGuid, - appModel.GetType(), - org, - app, - instanceOwnerId, - dataGuid - ); + await _dataClient.UpdateFormData(instance, appModel, dataElement); } catch (PlatformHttpException e) when (e.Response.StatusCode is HttpStatusCode.Forbidden) { diff --git a/src/Altinn.App.Api/Controllers/InstancesController.cs b/src/Altinn.App.Api/Controllers/InstancesController.cs index ee9eb882a..c11d3a124 100644 --- a/src/Altinn.App.Api/Controllers/InstancesController.cs +++ b/src/Altinn.App.Api/Controllers/InstancesController.cs @@ -15,7 +15,6 @@ using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Serialization; 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; @@ -62,7 +61,6 @@ public class InstancesController : ControllerBase private readonly IProfileClient _profileClient; private readonly IAppMetadata _appMetadata; - private readonly IAppModel _appModel; private readonly AppImplementationFactory _appImplementationFactory; private readonly IPDP _pdp; private readonly IPrefill _prefillService; @@ -87,7 +85,6 @@ public InstancesController( IInstanceClient instanceClient, IDataClient dataClient, IAppMetadata appMetadata, - IAppModel appModel, IAuthenticationContext authenticationContext, IPDP pdp, IEventsClient eventsClient, @@ -109,7 +106,6 @@ IServiceProvider serviceProvider _appMetadata = appMetadata; _altinnPartyClient = altinnPartyClient; _registerClient = serviceProvider.GetRequiredService(); - _appModel = appModel; _appImplementationFactory = serviceProvider.GetRequiredService(); _pdp = pdp; _eventsClient = eventsClient; @@ -960,8 +956,6 @@ private async Task CopyDataFromSourceInstance( Instance sourceInstance ) { - string org = application.Org; - string app = application.AppIdentifier.App; int instanceOwnerPartyId = int.Parse(targetInstance.InstanceOwner.PartyId, CultureInfo.InvariantCulture); string[] sourceSplit = sourceInstance.Id.Split("/"); @@ -987,28 +981,7 @@ Instance sourceInstance { DataType dt = dts.First(dt => dt.Id.Equals(de.DataType, StringComparison.Ordinal)); - Type type; - try - { - type = _appModel.GetModelType(dt.AppLogic.ClassRef); - } - catch (Exception altinnAppException) - { - throw new ServiceException( - HttpStatusCode.InternalServerError, - $"App.GetAppModelType failed: {altinnAppException.Message}", - altinnAppException - ); - } - - object data = await _dataClient.GetFormData( - sourceInstanceGuid, - type, - org, - app, - instanceOwnerPartyId, - Guid.Parse(de.Id) - ); + object data = await _dataClient.GetFormData(sourceInstance, de); if (application.CopyInstanceSettings.ExcludedDataFields != null) { @@ -1026,15 +999,7 @@ await _prefillService.PrefillDataModel( ObjectUtils.InitializeAltinnRowId(data); - await _dataClient.InsertFormData( - data, - Guid.Parse(targetInstance.Id.Split("/")[1]), - type, - org, - app, - instanceOwnerPartyId, - dt.Id - ); + await _dataClient.InsertFormData(targetInstance, dt.Id, data); await UpdatePresentationTextsOnInstance(application.PresentationFields, targetInstance, dt.Id, data); await UpdateDataValuesOnInstance(application.DataFields, targetInstance, dt.Id, data); @@ -1067,8 +1032,6 @@ await _dataClient.InsertFormData( if (binaryDataTypes.Any(dt => dt.Id.Equals(de.DataType, StringComparison.Ordinal))) { using var binaryDataStream = await _dataClient.GetBinaryData( - org, - app, instanceOwnerPartyId, sourceInstanceGuid, Guid.Parse(de.Id) diff --git a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs index 508f7a5ee..119777c83 100644 --- a/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs +++ b/src/Altinn.App.Core/EFormidling/Implementation/DefaultEFormidlingService.cs @@ -250,8 +250,6 @@ ValidAltinnEFormidlingConfiguration config usedFileNames.Add(uniqueFileName); await using Stream stream = await _dataClient.GetBinaryData( - applicationMetadata.Org, - applicationMetadata.AppIdentifier.App, instanceOwnerPartyId, instanceGuid, new Guid(dataElement.Id) diff --git a/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs b/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs index 2da086ccb..7bd57b23d 100644 --- a/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs +++ b/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs @@ -75,11 +75,7 @@ public async Task AuthorizeAction(UserActionAuthorizerContext context) var signatureDataElements = instance.Data.Where(d => dataTypes.Contains(d.DataType)).ToList(); foreach (var signatureDataElement in signatureDataElements) { - var signee = await GetSigneeFromSignDocument( - appMetadata.AppIdentifier, - context.InstanceIdentifier, - signatureDataElement - ); + var signee = await GetSigneeFromSignDocument(context.InstanceIdentifier, signatureDataElement); bool unauthorized = context.Authentication switch { Authenticated.User a => a.UserId.ToString(CultureInfo.InvariantCulture) == signee?.UserId, @@ -97,14 +93,11 @@ public async Task AuthorizeAction(UserActionAuthorizerContext context) } private async Task GetSigneeFromSignDocument( - 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) diff --git a/src/Altinn.App.Core/Features/Signing/Services/SigningReceiptService.cs b/src/Altinn.App.Core/Features/Signing/Services/SigningReceiptService.cs index a53a84e9d..a6e2b0ac3 100644 --- a/src/Altinn.App.Core/Features/Signing/Services/SigningReceiptService.cs +++ b/src/Altinn.App.Core/Features/Signing/Services/SigningReceiptService.cs @@ -206,8 +206,6 @@ IDataClient dataClient .WithSendersReference(element.Id) .WithData( await dataClient.GetDataBytes( - appMetadata.AppIdentifier.Org, - appMetadata.AppIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, Guid.Parse(element.Id) diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.DataClient.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.DataClient.cs index d171a8bdd..1d269cd4e 100644 --- a/src/Altinn.App.Core/Features/Telemetry/Telemetry.DataClient.cs +++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.DataClient.cs @@ -13,19 +13,19 @@ partial class Telemetry return activity; } - internal Activity? StartInsertFormDataActivity(Guid? instanceId, int? partyId) + internal Activity? StartUpdateDataActivity(Guid instanceId, Guid dataElementId) { - var activity = ActivitySource.StartActivity($"{Prefix}.InsertFormData"); + var activity = ActivitySource.StartActivity($"{Prefix}.UpdateData"); activity?.SetInstanceId(instanceId); - activity?.SetInstanceOwnerPartyId(partyId); + activity?.SetDataElementId(dataElementId); return activity; } - internal Activity? StartUpdateDataActivity(Guid instanceId, Guid dataElementId) + internal Activity? StartUpdateDataActivity(Instance instance, DataElement dataElement) { var activity = ActivitySource.StartActivity($"{Prefix}.UpdateData"); - activity?.SetInstanceId(instanceId); - activity?.SetDataElementId(dataElementId); + activity?.SetInstanceId(instance); + activity?.SetDataElementId(dataElement); return activity; } @@ -45,14 +45,6 @@ partial class Telemetry return activity; } - internal Activity? StartDeleteBinaryDataActivity(Guid? instanceId, int? partyId) - { - var activity = ActivitySource.StartActivity($"{Prefix}.DeleteBinaryData"); - activity?.SetInstanceId(instanceId); - activity?.SetInstanceOwnerPartyId(partyId); - return activity; - } - internal Activity? StartInsertBinaryDataActivity(Guid? instanceId, int? partyId) { var activity = ActivitySource.StartActivity($"{Prefix}.InsertBinaryData"); @@ -106,6 +98,13 @@ partial class Telemetry return activity; } + internal Activity? StartGetFormDataActivity(Instance? instance) + { + var activity = ActivitySource.StartActivity($"{Prefix}.GetFormData"); + activity?.SetInstanceId(instance); + return activity; + } + internal Activity? StartLockDataElementActivity(string? instanceId, Guid? dataGuid) { var activity = ActivitySource.StartActivity($"{Prefix}.LockDataElement"); diff --git a/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs b/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs index 55f8dc713..3cc08356f 100644 --- a/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs +++ b/src/Altinn.App.Core/Helpers/Serialization/ModelSerializationService.cs @@ -4,6 +4,7 @@ using System.Xml; using System.Xml.Serialization; using Altinn.App.Core.Features; +using Altinn.App.Core.Infrastructure.Clients.Storage; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Models.Result; using Altinn.Platform.Storage.Interface.Models; @@ -38,14 +39,43 @@ 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 + [Obsolete("DeserializeFromStorage needs a DataElement parameter to support json in storage")] public object DeserializeFromStorage(ReadOnlySpan data, DataType dataType) { + if (DataClient.TypeAllowsJson(dataType)) + { + throw new InvalidOperationException( + $"Data type {dataType.Id} allows application/json and must use DeserializeFromStorage with DataElement specified" + ); + } var type = GetModelTypeForDataType(dataType); - - // TODO: support sending json to storage based on dataType.ContentTypes return DeserializeXml(data, type); } + /// + /// Deserialize binary data from storage to a model of the classRef specified in the dataType + /// + /// 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, DataElement dataElement) + { + var type = GetModelTypeForDataType(dataType); + + return dataElement.ContentType?.ToLowerInvariant() switch + { + "application/xml" => DeserializeXml(data, type), + "application/json" => DeserializeJson(data, type), + null or "" => throw new InvalidOperationException( + $"Data element {dataElement.Id} does not have a content type" + ), // Consider defaulting to xml after initial testing? + _ => throw new InvalidOperationException( + $"Unsupported content type {dataElement.ContentType} on data element {dataElement.Id}" + ), + }; + } + /// /// Serialize an object to binary data for storage, respecting classRef and content type in dataType /// @@ -53,7 +83,31 @@ public object DeserializeFromStorage(ReadOnlySpan data, DataType dataType) /// The data type /// the binary data and the content type (currently only application/xml, but likely also json in the future) /// If the classRef in dataType does not match type of the model + [Obsolete("SerializeToStorage needs a DataElement parameter to support json in storage")] public (ReadOnlyMemory data, string contentType) SerializeToStorage(object model, DataType dataType) + { + if (DataClient.TypeAllowsJson(dataType)) + { + throw new InvalidOperationException( + $"Data type {dataType.Id} allows application/json and must use SerializeToStorage with DataElement specified" + ); + } + return SerializeToStorage(model, dataType, null); + } + + /// + /// Serialize an object to binary data for storage, respecting classRef and content type in dataType + /// + /// The object to serialize (must match the classRef in DataType) + /// The data type + /// The existing data element to preserve content type + /// 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, + DataElement? dataElement + ) { var type = GetModelTypeForDataType(dataType); if (type != model.GetType()) @@ -63,8 +117,15 @@ public object DeserializeFromStorage(ReadOnlySpan data, DataType dataType) ); } - //TODO: support sending json to storage based on dataType.ContentTypes - return (SerializeToXml(model), "application/xml"); + var contentType = dataElement is not null ? dataElement.ContentType : FindFirstContentType(dataType); + + return contentType?.ToLowerInvariant() switch + { + "application/json" => (SerializeToJson(model), "application/json"), + // When the content type is missing, default to xml for backward compatibility + "application/xml" or "" or null => (SerializeToXml(model), "application/xml"), + _ => throw new InvalidOperationException($"Unsupported content type {contentType}"), + }; } /// @@ -125,7 +186,7 @@ DataType dataType var modelType = GetModelTypeForDataType(dataType); object model; - if (contentType?.Contains("application/xml") ?? true) // default to xml if no content type is provided + if (contentType?.Contains("application/xml", StringComparison.Ordinal) ?? true) // default to xml if no content type is provided { try { @@ -145,7 +206,7 @@ DataType dataType }; } } - else if (contentType.Contains("application/json")) + else if (contentType.Contains("application/json", StringComparison.Ordinal)) { try { @@ -315,4 +376,26 @@ public object GetEmpty(DataType dataType) ArgumentException.ThrowIfNullOrWhiteSpace(dataType?.AppLogic?.ClassRef); return _appModel.Create(dataType.AppLogic.ClassRef); } + + private static string FindFirstContentType(DataType dataType) + { + if (dataType.AllowedContentTypes is not null) + { + foreach (var contentType in dataType.AllowedContentTypes) + { + if ("application/xml".Equals(contentType, StringComparison.OrdinalIgnoreCase)) + { + return "application/xml"; + } + if ("application/json".Equals(contentType, StringComparison.OrdinalIgnoreCase)) + { + return "application/json"; + } + } + } + + // If no supported content type is found, default to xml for backward compatibility + // Valid apps will never trigger this + return "application/xml"; + } } diff --git a/src/Altinn.App.Core/Implementation/DefaultAppEvents.cs b/src/Altinn.App.Core/Implementation/DefaultAppEvents.cs index 19a697a17..cc960d78b 100644 --- a/src/Altinn.App.Core/Implementation/DefaultAppEvents.cs +++ b/src/Altinn.App.Core/Implementation/DefaultAppEvents.cs @@ -68,8 +68,6 @@ private async Task AutoDeleteDataElements(Instance instance) { deleteTasks.Add( _dataClient.DeleteData( - applicationMetadata.Org, - applicationMetadata.AppIdentifier.App, int.Parse(instance.InstanceOwner.PartyId, CultureInfo.InvariantCulture), Guid.Parse(item.InstanceGuid), Guid.Parse(item.Id), diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index dde0630c6..591bc2beb 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -25,7 +25,7 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage; /// /// A client for handling actions on data in Altinn Platform. /// -public class DataClient : IDataClient +public sealed class DataClient : IDataClient { private readonly PlatformSettings _platformSettings; private readonly ILogger _logger; @@ -62,6 +62,7 @@ public DataClient(HttpClient httpClient, IServiceProvider serviceProvider) } /// + [Obsolete("Use InsertFormData with Instance parameter instead")] public async Task InsertFormData( T dataToSerialize, Guid instanceGuid, @@ -75,67 +76,47 @@ public async Task InsertFormData( ) where T : notnull { - using var cts = cancellationToken.WithTimeout(_httpOperationTimeout); - using var activity = _telemetry?.StartInsertFormDataActivity(instanceGuid, instanceOwnerPartyId); + if (type != dataToSerialize.GetType()) + { + throw new ArgumentException( + $"The provided type {type.FullName} does not match the type of dataToSerialize {dataToSerialize.GetType().FullName}" + ); + } Instance instance = new() { Id = $"{instanceOwnerPartyId}/{instanceGuid}" }; - return await InsertFormData(instance, dataType, dataToSerialize, type, authenticationMethod, cts.Token); + return await InsertFormData(instance, dataType, dataToSerialize, authenticationMethod, cancellationToken); } /// - public async Task InsertFormData( + public async Task InsertFormData( Instance instance, - string dataTypeString, - T dataToSerialize, - Type type, + string dataTypeId, + object dataToSerialize, StorageAuthenticationMethod? authenticationMethod = null, CancellationToken cancellationToken = default ) - where T : notnull { - using var cts = cancellationToken.WithTimeout(_httpOperationTimeout); using var activity = _telemetry?.StartInsertFormDataActivity(instance); - string apiUrl = $"instances/{instance.Id}/data?dataType={dataTypeString}"; - - JwtToken token = await _authenticationTokenResolver.GetAccessToken( - authenticationMethod ?? _defaultAuthenticationMethod, - cancellationToken: cts.Token - ); - - var application = await _appMetadata.GetApplicationMetadata(); + var appMetadata = await _appMetadata.GetApplicationMetadata(); var dataType = - application.DataTypes.Find(d => d.Id == dataTypeString) - ?? throw new InvalidOperationException($"Data type {dataTypeString} not found in applicationmetadata.json"); - - var (data, contentType) = _modelSerializationService.SerializeToStorage(dataToSerialize, dataType); - - StreamContent streamContent = new(new MemoryAsStream(data)); - streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - HttpResponseMessage response = await _client.PostAsync( - token, - apiUrl, - streamContent, - cancellationToken: cts.Token - ); - - if (response.IsSuccessStatusCode) - { - string instanceData = await response.Content.ReadAsStringAsync(cts.Token); - // ! TODO: this null-forgiving operator should be fixed/removed for the next major release - var dataElement = JsonConvert.DeserializeObject(instanceData)!; + appMetadata.DataTypes.Find(d => d.Id == dataTypeId) + ?? throw new InvalidOperationException($"Data type {dataTypeId} not found in applicationmetadata.json"); - return dataElement; - } + var (data, contentType) = _modelSerializationService.SerializeToStorage(dataToSerialize, dataType, null); - _logger.Log( - LogLevel.Error, - "unable to save form data for instance {InstanceId} due to response {StatusCode}", + return await InsertBinaryData( instance.Id, - response.StatusCode + dataTypeId, + contentType, + null, + new MemoryAsStream(data), + null, + authenticationMethod, + cancellationToken ); - throw await PlatformHttpException.CreateAsync(response); } /// + [Obsolete("Use the UpdateFormData method with Instance parameter instead")] public async Task UpdateData( T dataToSerialize, Guid instanceGuid, @@ -151,6 +132,22 @@ public async Task UpdateData( { using var cts = cancellationToken.WithTimeout(_httpOperationTimeout); using var activity = _telemetry?.StartUpdateDataActivity(instanceGuid, dataId); + // Verify that all dataTypes uses XML serialization so that the deprecated method might work as expected without access to dataElement.ContentType + if (type != dataToSerialize.GetType()) + { + throw new ArgumentException( + $"The provided type {type.FullName} does not match the type of dataToSerialize {dataToSerialize.GetType().FullName}" + ); + } + var classRef = type.FullName; + var appMetadata = await _appMetadata.GetApplicationMetadata(); + if (TypeAllowsJson(classRef, appMetadata)) + { + throw new InvalidOperationException( + $"The data type {classRef} is configured to use JSON serialization and must use UpdateFormData method instead" + ); + } + string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; @@ -159,8 +156,6 @@ public async Task UpdateData( cancellationToken: cts.Token ); - //TODO: this method does not get enough information to know the content type from the DataType - // if we start to support more than XML var serializedBytes = _modelSerializationService.SerializeToXml(dataToSerialize); var contentType = "application/xml"; @@ -185,10 +180,40 @@ public async Task UpdateData( throw await PlatformHttpException.CreateAsync(response); } + /// + public async Task UpdateFormData( + Instance instance, + object dataToSerialize, + DataElement dataElement, + StorageAuthenticationMethod? authenticationMethod = null, + CancellationToken cancellationToken = default + ) + { + using var activity = _telemetry?.StartUpdateDataActivity(instance, dataElement); + + var appMetadata = await _appMetadata.GetApplicationMetadata(); + + var dataType = + appMetadata.DataTypes.Find(d => d.Id == dataElement.DataType) + ?? throw new InvalidOperationException( + $"Data type {dataElement.DataType} not found in applicationmetadata.json" + ); + + var (data, contentType) = _modelSerializationService.SerializeToStorage(dataToSerialize, dataType, dataElement); + + return await UpdateBinaryData( + new InstanceIdentifier(instance), + contentType, + dataElement.Filename, + Guid.Parse(dataElement.Id), + new MemoryAsStream(data), + authenticationMethod, + cancellationToken + ); + } + /// public async Task GetBinaryData( - string org, - string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataId, @@ -212,11 +237,11 @@ public async Task GetBinaryData( { return await response.Content.ReadAsStreamAsync(cts.Token); } - else if (response.StatusCode == HttpStatusCode.NotFound) + + if (response.StatusCode == HttpStatusCode.NotFound) { -#nullable disable - return null; -#nullable restore + // ! TODO: Remove null return in v9 and throw exception instead + return null!; } throw await PlatformHttpException.CreateAsync(response); @@ -262,6 +287,7 @@ public async Task GetBinaryDataStream( } /// + [Obsolete("Use the overload with Instance parameter instead")] public async Task GetFormData( Guid instanceGuid, Type type, @@ -283,6 +309,15 @@ public async Task GetFormData( cancellationToken: cts.Token ); + var classRef = type.FullName; + var appMetadata = await _appMetadata.GetApplicationMetadata(); + if (TypeAllowsJson(classRef, appMetadata)) + { + throw new InvalidOperationException( + $"The data type {classRef} is configured to use JSON serialization and must use GetFormData with dataElement argument instead" + ); + } + HttpResponseMessage response = await _client.GetAsync(token, apiUrl, cancellationToken: cts.Token); if (response.IsSuccessStatusCode) { @@ -290,8 +325,6 @@ public async Task GetFormData( try { - //TODO: this method does not get enough information to know the content type from the DataType - // if we start to support more than XML return _modelSerializationService.DeserializeXml(bytes, type); } catch (Exception e) @@ -304,10 +337,39 @@ public async Task GetFormData( throw await PlatformHttpException.CreateAsync(response); } + /// + public async Task GetFormData( + Instance instance, + DataElement dataElement, + StorageAuthenticationMethod? authenticationMethod = null, + CancellationToken cancellationToken = default + ) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentNullException.ThrowIfNull(dataElement); + using var activity = _telemetry?.StartGetFormDataActivity(instance); + + var appMetadata = await _appMetadata.GetApplicationMetadata(); + var dataType = + appMetadata.DataTypes.Find(d => d.Id == dataElement.DataType) + ?? throw new InvalidOperationException( + $"Data type {dataElement.DataType} not found in applicationmetadata.json" + ); + + InstanceIdentifier instanceIdentifier = new(instance); + var data = await GetDataBytes( + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + Guid.Parse(dataElement.Id), + authenticationMethod, + cancellationToken + ); + + return _modelSerializationService.DeserializeFromStorage(data, dataType, dataElement); + } + /// public async Task GetDataBytes( - string org, - string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataId, @@ -337,8 +399,6 @@ public async Task GetDataBytes( /// public async Task> GetBinaryDataList( - string org, - string app, int instanceOwnerPartyId, Guid instanceGuid, StorageAuthenticationMethod? authenticationMethod = null, @@ -411,23 +471,8 @@ private static void ExtractAttachments(List dataList, List - public async Task DeleteBinaryData( - string org, - string app, - int instanceOwnerPartyId, - Guid instanceGuid, - Guid dataGuid - ) - { - using var activity = _telemetry?.StartDeleteBinaryDataActivity(instanceGuid, instanceOwnerPartyId); - return await DeleteData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, false); - } - /// public async Task DeleteData( - string org, - string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid, @@ -460,6 +505,7 @@ public async Task DeleteData( } /// + [Obsolete("The overload that takes a HttpRequest is deprecated, use the overload that takes a Stream instead")] public async Task InsertBinaryData( string org, string app, @@ -548,12 +594,16 @@ public async Task InsertBinaryData( } _logger.LogError( - $"Storing attachment for instance {instanceId} failed with status code {response.StatusCode} - content {await response.Content.ReadAsStringAsync(cts.Token)}" + "Storing attachment for instance {InstanceId} failed with status code {StatusCode} - content {Content}", + instanceId, + response.StatusCode, + await response.Content.ReadAsStringAsync(cts.Token) ); throw await PlatformHttpException.CreateAsync(response); } /// + [Obsolete("The overload that takes a HttpRequest is deprecated, use the overload that takes a Stream instead")] public async Task UpdateBinaryData( string org, string app, @@ -759,4 +809,18 @@ await response.Content.ReadAsStringAsync(cts.Token) ); throw await PlatformHttpException.CreateAsync(response); } + + private static bool TypeAllowsJson(string? classRef, ApplicationMetadata appMetadata) + { + return appMetadata.DataTypes.Where(dt => dt?.AppLogic?.ClassRef == classRef).Any(TypeAllowsJson); + } + + internal static bool TypeAllowsJson(DataType? dataType) + { + if (dataType?.AllowedContentTypes is null) + return false; + return !dataType.AllowedContentTypes.TrueForAll(ct => + !ct.Equals("application/json", StringComparison.OrdinalIgnoreCase) + ); + } } diff --git a/src/Altinn.App.Core/Internal/Data/DataService.cs b/src/Altinn.App.Core/Internal/Data/DataService.cs index bbe27960a..af9d41d0f 100644 --- a/src/Altinn.App.Core/Internal/Data/DataService.cs +++ b/src/Altinn.App.Core/Internal/Data/DataService.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using Altinn.App.Core.Internal.App; using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; @@ -11,17 +10,13 @@ internal class DataService : IDataService private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); private readonly IDataClient _dataClient; - private readonly IAppMetadata _appMetadata; /// /// Initializes a new instance of the class. /// - /// - /// - public DataService(IDataClient dataClient, IAppMetadata appMetadata) + public DataService(IDataClient dataClient) { _dataClient = dataClient; - _appMetadata = appMetadata; } /// @@ -94,11 +89,7 @@ object data /// public async Task DeleteById(InstanceIdentifier instanceIdentifier, Guid dataElementId) { - ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); - return await _dataClient.DeleteData( - applicationMetadata.AppIdentifier.Org, - applicationMetadata.AppIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, dataElementId, @@ -108,11 +99,7 @@ public async Task DeleteById(InstanceIdentifier instanceIdentifier, Guid d private async Task GetDataForDataElement(InstanceIdentifier instanceIdentifier, DataElement dataElement) { - ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); - Stream dataStream = await _dataClient.GetBinaryData( - applicationMetadata.AppIdentifier.Org, - applicationMetadata.AppIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, new Guid(dataElement.Id) diff --git a/src/Altinn.App.Core/Internal/Data/IDataClient.cs b/src/Altinn.App.Core/Internal/Data/IDataClient.cs index 0eb883bca..76330507f 100644 --- a/src/Altinn.App.Core/Internal/Data/IDataClient.cs +++ b/src/Altinn.App.Core/Internal/Data/IDataClient.cs @@ -23,6 +23,7 @@ public interface IDataClient /// The data type to create, must be a valid data type defined in application metadata /// An optional specification of the authentication method to use for requests /// An optional cancellation token + [Obsolete("Use InsertFormData with Instance parameter instead")] Task InsertFormData( T dataToSerialize, Guid instanceGuid, @@ -47,6 +48,7 @@ Task InsertFormData( /// An optional specification of the authentication method to use for requests /// An optional cancellation token /// The data element metadata + [Obsolete("Use the overload without Type parameter")] Task InsertFormData( Instance instance, string dataTypeString, @@ -55,7 +57,33 @@ Task InsertFormData( StorageAuthenticationMethod? authenticationMethod = null, CancellationToken cancellationToken = default ) - where T : notnull; + where T : notnull + { + if (dataToSerialize.GetType() != type) + { + throw new ArgumentException( + $"The provided type {type.FullName} does not match the type of dataToSerialize {dataToSerialize.GetType().FullName}" + ); + } + return InsertFormData(instance, dataTypeString, dataToSerialize, authenticationMethod, cancellationToken); + } + + /// + /// Creates a new data element for the given instance and data model + /// + /// The instance to add the element to + /// The id of the data type to add + /// An instance of the class for the form data + /// An optional specification of the authentication method to use for requests + /// An optional cancellation token + /// + Task InsertFormData( + Instance instance, + string dataTypeId, + object dataToSerialize, + StorageAuthenticationMethod? authenticationMethod = null, + CancellationToken cancellationToken = default + ); /// /// updates the form data @@ -70,7 +98,7 @@ Task InsertFormData( /// the data id /// An optional specification of the authentication method to use for requests /// An optional cancellation token - //TODO: [Obsolete in v9 in favour of a version that gets the dataType so we can support json and xml] + [Obsolete("Use the UpdateFormData method with Instance parameter instead")] Task UpdateData( T dataToSerialize, Guid instanceGuid, @@ -84,6 +112,22 @@ Task UpdateData( ) where T : notnull; + /// + /// updates the form data + /// + /// The instance + /// The form data to serialize + /// The data element + /// An optional specification of the authentication method to use for requests + /// An optional cancellation token + Task UpdateFormData( + Instance instance, + object dataToSerialize, + DataElement dataElement, + StorageAuthenticationMethod? authenticationMethod = null, + CancellationToken cancellationToken = default + ); + /// /// Gets the form data /// @@ -95,6 +139,7 @@ Task UpdateData( /// the data id /// An optional specification of the authentication method to use for requests /// An optional cancellation token + [Obsolete("Use the overload with Instance parameter instead")] Task GetFormData( Guid instanceGuid, Type type, @@ -106,6 +151,21 @@ Task GetFormData( CancellationToken cancellationToken = default ); + /// + /// Gets the deserialized form data + /// + /// The instance + /// The data element + /// An optional specification of the authentication method to use for requests + /// An optional cancellation token + /// + Task GetFormData( + Instance instance, + DataElement dataElement, + StorageAuthenticationMethod? authenticationMethod = null, + CancellationToken cancellationToken = default + ); + /// /// Gets the data as is. Note: This method buffers the entire response in memory before returning the stream. /// For memory-efficient processing of large files, use instead. @@ -117,6 +177,7 @@ Task GetFormData( /// the data id /// An optional specification of the authentication method to use for requests /// An optional cancellation token + [Obsolete("Org and App parameters are not used, use the overload without these parameters")] Task GetBinaryData( string org, string app, @@ -125,6 +186,22 @@ Task GetBinaryData( Guid dataId, StorageAuthenticationMethod? authenticationMethod = null, CancellationToken cancellationToken = default + ) => GetBinaryData(instanceOwnerPartyId, instanceGuid, dataId, authenticationMethod, cancellationToken); + + /// + /// Gets the data as is. + /// + /// The instance owner id + /// The instance id + /// the data id + /// An optional specification of the authentication method to use for requests + /// An optional cancellation token + Task GetBinaryData( + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataId, + StorageAuthenticationMethod? authenticationMethod = null, + CancellationToken cancellationToken = default ); /// @@ -148,7 +225,7 @@ Task GetBinaryDataStream( ); /// - /// Similar to GetBinaryData, but returns a HttpResponseMessage instead of a cached stream + /// Similar to GetBinaryData, but returns the raw bytes instead of a cached stream /// /// Unique identifier of the organisation responsible for the app. /// Application identifier which is unique within an organisation. @@ -158,6 +235,7 @@ Task GetBinaryDataStream( /// An optional specification of the authentication method to use for requests /// An optional cancellation token /// The raw HttpResponseMessage from the call to platform + [Obsolete("Org and App parameters are not used, use the overload without these parameters")] Task GetDataBytes( string org, string app, @@ -166,6 +244,23 @@ Task GetDataBytes( Guid dataId, StorageAuthenticationMethod? authenticationMethod = null, CancellationToken cancellationToken = default + ) => GetDataBytes(instanceOwnerPartyId, instanceGuid, dataId, authenticationMethod, cancellationToken); + + /// + /// Similar to GetBinaryData, but returns the raw bytes instead of a cached stream + /// + /// The instance owner id + /// The instance id + /// the data id + /// An optional specification of the authentication method to use for requests + /// An optional cancellation token + /// The raw bytes from storage + Task GetDataBytes( + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataId, + StorageAuthenticationMethod? authenticationMethod = null, + CancellationToken cancellationToken = default ); /// @@ -178,6 +273,7 @@ Task GetDataBytes( /// An optional specification of the authentication method to use for requests /// An optional cancellation token /// A list with attachments metadata ordered by attachmentType + [Obsolete("Org and App parameters are not used, use the overload without these parameters")] Task> GetBinaryDataList( string org, string app, @@ -185,6 +281,21 @@ Task> GetBinaryDataList( Guid instanceGuid, StorageAuthenticationMethod? authenticationMethod = null, CancellationToken cancellationToken = default + ) => GetBinaryDataList(instanceOwnerPartyId, instanceGuid, authenticationMethod, cancellationToken); + + /// + /// Method that gets metadata on form attachments ordered by attachmentType + /// + /// The instance owner id + /// The instance id + /// An optional specification of the authentication method to use for requests + /// An optional cancellation token + /// A list with attachments metadata ordered by attachmentType + Task> GetBinaryDataList( + int instanceOwnerPartyId, + Guid instanceGuid, + StorageAuthenticationMethod? authenticationMethod = null, + CancellationToken cancellationToken = default ); /// @@ -196,7 +307,8 @@ Task> GetBinaryDataList( /// 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); + Task DeleteBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid) => + DeleteData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, false); /// /// Method that removes a data elemen from disk/storage immediatly or marks it as deleted. @@ -209,6 +321,7 @@ Task> GetBinaryDataList( /// A boolean indicating whether or not the delete should be executed immediately or delayed /// An optional specification of the authentication method to use for requests /// An optional cancellation token + [Obsolete("Use the overload without org and app parameters")] Task DeleteData( string org, string app, @@ -218,6 +331,24 @@ Task DeleteData( bool delay, StorageAuthenticationMethod? authenticationMethod = null, CancellationToken cancellationToken = default + ) => DeleteData(instanceOwnerPartyId, instanceGuid, dataGuid, delay, authenticationMethod, cancellationToken); + + /// + /// Method that removes a data elemen from disk/storage immediatly or marks it as deleted. + /// + /// The instance owner id + /// The instance id + /// The attachment id + /// A boolean indicating whether or not the delete should be executed immediately or delayed + /// An optional specification of the authentication method to use for requests + /// An optional cancellation token + Task DeleteData( + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataGuid, + bool delay, + StorageAuthenticationMethod? authenticationMethod = null, + CancellationToken cancellationToken = default ); /// @@ -231,6 +362,7 @@ Task DeleteData( /// Http request containing the attachment to be saved /// An optional specification of the authentication method to use for requests /// An optional cancellation token + [Obsolete("The overload that takes a HttpRequest is deprecated, use the overload that takes a Stream instead")] Task InsertBinaryData( string org, string app, @@ -353,3 +485,38 @@ Task UnlockDataElement( CancellationToken cancellationToken = default ); } + +/// +/// Extension methods for IDataClient +/// +public static class IDataClientExtensions +{ + /// + /// Gets the deserialized form data + /// + /// The type + /// The data client + /// The instance + /// The data element + /// An optional specification of the authentication method to use for requests + /// An optional cancellation token + public static async Task GetFormData( + this IDataClient dataClient, + Instance instance, + DataElement dataElement, + StorageAuthenticationMethod? authenticationMethod = null, + CancellationToken cancellationToken = default + ) + { + object formData = await dataClient.GetFormData(instance, dataElement, authenticationMethod, cancellationToken); + + if (formData is T typedFormData) + { + return typedFormData; + } + + throw new InvalidCastException( + $"Failed to cast form data of type {formData?.GetType().FullName ?? "null"} to requested type {typeof(T).FullName}" + ); + } +} diff --git a/src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs b/src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs index 1b9a4a7b0..aed0c5481 100644 --- a/src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs +++ b/src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs @@ -131,9 +131,10 @@ public async Task GetFormDataWrapper(DataElementIdentifier dat ); } var binaryData = await GetBinaryData(dataElementIdentifier); + var dataElement = GetDataElement(dataElementIdentifier); return FormDataWrapperFactory.Create( - _modelSerializationService.DeserializeFromStorage(binaryData.Span, dataType) + _modelSerializationService.DeserializeFromStorage(binaryData.Span, dataType, dataElement) ); } ); @@ -214,8 +215,6 @@ public async Task> GetBinaryData(DataElementIdentifier data dataElementIdentifier, async () => await _dataClient.GetDataBytes( - _appMetadata.AppIdentifier.Org, - _appMetadata.AppIdentifier.App, _instanceOwnerPartyId, _instanceGuid, dataElementIdentifier.Guid, @@ -259,7 +258,7 @@ public FormDataChange AddFormDataElement(string dataTypeId, object model) } ObjectUtils.InitializeAltinnRowId(model); - var (bytes, contentType) = _modelSerializationService.SerializeToStorage(model, dataType); + var (bytes, contentType) = _modelSerializationService.SerializeToStorage(model, dataType, null); FormDataChange change = new FormDataChange( type: ChangeType.Created, @@ -426,7 +425,8 @@ out ReadOnlyMemory cachedBinary var (currentBinary, _) = _modelSerializationService.SerializeToStorage( dataWrapper.BackingData(), - dataType + dataType, + dataElement ); if (!currentBinary.Span.SequenceEqual(cachedBinary.Span)) @@ -441,7 +441,7 @@ out ReadOnlyMemory cachedBinary // For patch requests we could get the previous data from the patch, but it's not available here // and deserializing twice is not a big deal previousFormDataWrapper: FormDataWrapperFactory.Create( - _modelSerializationService.DeserializeFromStorage(cachedBinary.Span, dataType) + _modelSerializationService.DeserializeFromStorage(cachedBinary.Span, dataType, dataElement) ), currentBinaryData: currentBinary, previousBinaryData: cachedBinary @@ -537,8 +537,6 @@ internal async Task UpdateInstanceData(DataElementChanges changes) async Task DeleteData() { await _dataClient.DeleteData( - _appMetadata.AppIdentifier.Org, - _appMetadata.AppIdentifier.App, _instanceOwnerPartyId, _instanceGuid, change.DataElementIdentifier.Guid, diff --git a/src/Altinn.App.Core/Internal/Data/PreviousDataAccessor.cs b/src/Altinn.App.Core/Internal/Data/PreviousDataAccessor.cs index 157a0eefd..9f1bd32d2 100644 --- a/src/Altinn.App.Core/Internal/Data/PreviousDataAccessor.cs +++ b/src/Altinn.App.Core/Internal/Data/PreviousDataAccessor.cs @@ -68,10 +68,11 @@ public async Task GetFormDataWrapper(DataElementIdentifier dat $"Data element {id.Id} is of data type {dataType.Id} which doesn't have app logic in application metadata and cant be used as form data" ); } - + var dataElement = GetDataElement(id); var binaryData = await _dataAccessor.GetBinaryData(id).ConfigureAwait(false); + return FormDataWrapperFactory.Create( - _modelSerializationService.DeserializeFromStorage(binaryData.Span, dataType) + _modelSerializationService.DeserializeFromStorage(binaryData.Span, dataType, dataElement) ); } ) diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs index f78cbedbb..e8894609b 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskCleaner.cs @@ -20,7 +20,6 @@ public ProcessTaskCleaner(ILogger logger, IDataClient dataCl /// public async Task RemoveAllDataElementsGeneratedFromTask(Instance instance, string taskId) { - AppIdentifier appIdentifier = new(instance.AppId); InstanceIdentifier instanceIdentifier = new(instance); var dataElements = instance @@ -39,8 +38,6 @@ public async Task RemoveAllDataElementsGeneratedFromTask(Instance instance, stri dataElement.BlobStoragePath ); await _dataClient.DeleteData( - appIdentifier.Org, - appIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, Guid.Parse(dataElement.Id), diff --git a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs index 4ce9b5de3..b1a2c1847 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessTasks/Common/ProcessTaskInitializer.cs @@ -87,12 +87,10 @@ DataType dataType in applicationMetadata.DataTypes.Where(dt => var instantiationProcessor = _appImplementationFactory.GetRequired(); await instantiationProcessor.DataCreation(instance, data, prefill); - Type type = _appModel.GetModelType(dataType.AppLogic.ClassRef); - ObjectUtils.InitializeAltinnRowId(data); ObjectUtils.PrepareModelForXmlStorage(data); - DataElement createdDataElement = await _dataClient.InsertFormData(instance, dataType.Id, data, type); + DataElement createdDataElement = await _dataClient.InsertFormData(instance, dataType.Id, data); instance.Data ??= []; instance.Data.Add(createdDataElement); diff --git a/src/Altinn.App.Core/Internal/Texts/TranslationService.cs b/src/Altinn.App.Core/Internal/Texts/TranslationService.cs index 87b2675ca..7c9c05cd4 100644 --- a/src/Altinn.App.Core/Internal/Texts/TranslationService.cs +++ b/src/Altinn.App.Core/Internal/Texts/TranslationService.cs @@ -262,7 +262,7 @@ await state.GetModelData(binding, context?.DataElementIdentifier, context?.RowIn if (_appMetadata is not null) { var appMetadata = await _appMetadata.GetApplicationMetadata(); - if (appMetadata?.Title.Count > 0) + if (appMetadata?.Title?.Count > 0) { return appMetadata.Title.TryGetValue(language, out var title) ? new TextResourceElement() { Id = "appName", Value = title } @@ -297,15 +297,15 @@ await state.GetModelData(binding, context?.DataElementIdentifier, context?.RowIn { Id = "backend.pdf_default_file_name", Value = "{0}.pdf", - Variables = new List() - { + Variables = + [ new TextResourceVariable() { Key = "appName", DataSource = "text", DefaultValue = "Altinn PDF", }, - }, + ], }; case "pdfPreviewText": return new TextResourceElement() diff --git a/src/Altinn.App.Core/Models/DataElementChanges.cs b/src/Altinn.App.Core/Models/DataElementChanges.cs index 44966e348..bdf4b9edc 100644 --- a/src/Altinn.App.Core/Models/DataElementChanges.cs +++ b/src/Altinn.App.Core/Models/DataElementChanges.cs @@ -34,7 +34,12 @@ internal DataElementChanges(IReadOnlyList allChanges) /// public abstract class DataElementChange { - internal DataElementChange(ChangeType type, DataType dataType, string contentType, DataElement? dataElement = null) + private protected DataElementChange( + ChangeType type, + DataType dataType, + string contentType, + DataElement? dataElement = null + ) { Type = type; DataElement = dataElement; @@ -177,7 +182,7 @@ internal FormDataChange( /// is still valid for editing, and the binary data can't keep up with the /// changes /// - /// Availible during validation, because then the data should not be + /// Available during validation, because then the data should not be /// changed, and it is used for storing and for verification that validators /// does not mutate the data. /// diff --git a/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs index 4d157eb9b..9619fdd63 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs @@ -690,23 +690,11 @@ public async Task HandleAction(UserActionContext context) data.TestCustomButtonReadOnlyInput = "Her kommer det data fra backend"; break; case "updateObsolete": - var instanceId = context.Instance.Id; - var instanceGuid = Guid.Parse(instanceId.Split('/')[1]); - var instanceOwner = int.Parse(instanceId.Split('/')[0]); - var dataGuid = Guid.Parse(context.Instance.Data.Single().Id); - - var obsoleteData = (Scheme) - await _dataClient.GetFormData( - instanceGuid, - typeof(Scheme), - context.Instance.Org, - context.Instance.AppId.Split('/')[1], - instanceOwner, - dataGuid - ); + var dataElement = context.Instance.Data.Single(); + var obsoleteData = await _dataClient.GetFormData(context.Instance, dataElement); obsoleteData.description = "Obsolete data"; var result = UserActionResult.SuccessResult(new List()); - result.AddUpdatedDataModel(dataGuid.ToString(), obsoleteData); + result.AddUpdatedDataModel(dataElement.Id, obsoleteData); return result; case "delete": var elementToDelete = context.DataMutator.GetDataElementsForType("Scheme").First(); diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesControllerFixture.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesControllerFixture.cs index f8de9d094..8ccdba524 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesControllerFixture.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesControllerFixture.cs @@ -112,27 +112,27 @@ internal static InstancesControllerFixture Create(Authenticated? auth = null) services.AddOptions().Configure(_ => { }); services.AddAppImplementationFactory(); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); - services.AddSingleton(new Mock().Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Loose).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Loose).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Loose).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); + services.AddSingleton(new Mock(MockBehavior.Strict).Object); services.AddSingleton(new Mock(MockBehavior.Strict).Object); - var httpContextMock = new Mock(); + var httpContextMock = new Mock(MockBehavior.Strict); services.AddTransient(_ => httpContextMock.Object); services.AddTransient(); diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs index b9ccb89ce..0fad2bac6 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_ActiveInstancesTests.cs @@ -82,7 +82,7 @@ public async Task UnknownUser_ReturnsEmptyString() .Mock() .Setup(c => c.GetInstances(It.IsAny>())) .ReturnsAsync(instances); - // _profile.Setup(p=>p.GetUserProfile(12345)).ReturnsAsync(default(UserProfile)!); + fixture.Mock().Setup(p => p.GetUserProfile(12345)).ReturnsAsync(default(UserProfile)!); // Act var controller = fixture.ServiceProvider.GetRequiredService(); diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs index 68f2a991b..5f984c097 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs @@ -394,12 +394,8 @@ public async Task CopyInstance_EverythingIsFine_ReturnsRedirect() .Mock() .Setup(p => p.GetFormData( - instanceGuid, - It.IsAny()!, - Org, - AppName, - instanceOwnerPartyId, - dataGuid, + It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny() ) @@ -409,13 +405,9 @@ public async Task CopyInstance_EverythingIsFine_ReturnsRedirect() .Mock() .Setup(p => p.InsertFormData( - It.IsAny(), - instanceGuid, - It.IsAny()!, - Org, - AppName, - instanceOwnerPartyId, + It.IsAny(), dataTypeId, + It.IsAny(), It.IsAny(), It.IsAny() ) @@ -524,12 +516,8 @@ public async Task CopyInstance_WithBinaryData_CopiesBothFormAndBinaryData() .Mock() .Setup(p => p.GetFormData( - instanceGuid, - It.IsAny()!, - Org, - AppName, - instanceOwnerPartyId, - formDataGuid, + It.Is(i => i.Id == instance.Id), + It.Is(d => d.Id == formDataGuid.ToString()), It.IsAny(), It.IsAny() ) @@ -539,13 +527,9 @@ public async Task CopyInstance_WithBinaryData_CopiesBothFormAndBinaryData() .Mock() .Setup(p => p.InsertFormData( - It.IsAny(), - instanceGuid, - It.IsAny()!, - Org, - AppName, - instanceOwnerPartyId, + It.Is(i => i.Id == instance.Id), formDataTypeId, + It.IsAny(), It.IsAny(), It.IsAny() ) @@ -556,8 +540,6 @@ public async Task CopyInstance_WithBinaryData_CopiesBothFormAndBinaryData() .Mock() .Setup(p => p.GetBinaryData( - Org, - AppName, instanceOwnerPartyId, instanceGuid, binaryDataGuid, @@ -595,12 +577,8 @@ public async Task CopyInstance_WithBinaryData_CopiesBothFormAndBinaryData() .Verify( p => p.GetFormData( - instanceGuid, - It.IsAny()!, - Org, - AppName, - instanceOwnerPartyId, - formDataGuid, + It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny() ), @@ -611,27 +589,21 @@ public async Task CopyInstance_WithBinaryData_CopiesBothFormAndBinaryData() .Verify( p => p.InsertFormData( - It.IsAny(), - instanceGuid, - It.IsAny()!, - Org, - AppName, - instanceOwnerPartyId, + It.IsAny(), formDataTypeId, + It.IsAny(), It.IsAny(), It.IsAny() ), Times.Once ); - // Verify binary data was copied (this should FAIL until we implement it) + // Verify binary data was copied fixture .Mock() .Verify( p => p.GetBinaryData( - Org, - AppName, instanceOwnerPartyId, instanceGuid, binaryDataGuid, @@ -743,12 +715,8 @@ public async Task CopyInstance_WithExcludedBinaryDataType_SkipsExcludedType() .Mock() .Setup(p => p.GetFormData( - instanceGuid, - It.IsAny()!, - Org, - AppName, - instanceOwnerPartyId, - formDataGuid, + It.Is(i => i.Id == instance.Id), + It.Is(d => d.Id == formDataGuid.ToString()), It.IsAny(), It.IsAny() ) @@ -758,13 +726,9 @@ public async Task CopyInstance_WithExcludedBinaryDataType_SkipsExcludedType() .Mock() .Setup(p => p.InsertFormData( - It.IsAny(), - instanceGuid, - It.IsAny()!, - Org, - AppName, - instanceOwnerPartyId, + It.Is(i => i.Id == instance.Id), formDataTypeId, + It.IsAny(), It.IsAny(), It.IsAny() ) @@ -776,8 +740,6 @@ public async Task CopyInstance_WithExcludedBinaryDataType_SkipsExcludedType() .Mock() .Setup(p => p.GetBinaryData( - Org, - AppName, instanceOwnerPartyId, instanceGuid, binaryDataGuid, @@ -815,12 +777,8 @@ public async Task CopyInstance_WithExcludedBinaryDataType_SkipsExcludedType() .Verify( p => p.GetFormData( - instanceGuid, - It.IsAny()!, - Org, - AppName, - instanceOwnerPartyId, - formDataGuid, + It.Is(i => i.Id == instance.Id), + It.Is(d => d.Id == formDataGuid.ToString()), It.IsAny(), It.IsAny() ), @@ -831,13 +789,9 @@ public async Task CopyInstance_WithExcludedBinaryDataType_SkipsExcludedType() .Verify( p => p.InsertFormData( - It.IsAny(), - instanceGuid, - It.IsAny()!, - Org, - AppName, - instanceOwnerPartyId, + It.Is(i => i.Id == instance.Id), formDataTypeId, + It.IsAny(), It.IsAny(), It.IsAny() ), @@ -850,8 +804,6 @@ public async Task CopyInstance_WithExcludedBinaryDataType_SkipsExcludedType() .Verify( p => p.GetBinaryData( - Org, - AppName, instanceOwnerPartyId, instanceGuid, binaryDataGuid, @@ -961,8 +913,6 @@ public async Task CopyInstance_IncludeAttachmentsIsTrue_CopiesBinaryData() .Mock() .Setup(p => p.GetBinaryData( - Org, - AppName, instanceOwnerPartyId, instanceGuid, binaryDataGuid, @@ -1000,8 +950,6 @@ public async Task CopyInstance_IncludeAttachmentsIsTrue_CopiesBinaryData() .Verify( p => p.GetBinaryData( - Org, - AppName, instanceOwnerPartyId, instanceGuid, binaryDataGuid, @@ -1061,6 +1009,31 @@ public async Task CopyInstance_IncludeAttachmentsIsTrue_CopiesBinaryData() ), Times.Never ); + fixture + .Mock() + .Verify( + p => + p.GetFormData( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Never + ); + fixture + .Mock() + .Verify( + p => + p.InsertFormData( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Never + ); } [Fact] @@ -1145,8 +1118,6 @@ public async Task CopyInstance_OnlyBinaryData_NotCopiedByDefault() .Mock() .Setup(p => p.GetBinaryData( - Org, - AppName, instanceOwnerPartyId, instanceGuid, binaryDataGuid, @@ -1184,8 +1155,6 @@ public async Task CopyInstance_OnlyBinaryData_NotCopiedByDefault() .Verify( p => p.GetBinaryData( - Org, - AppName, instanceOwnerPartyId, instanceGuid, binaryDataGuid, diff --git a/test/Altinn.App.Api.Tests/Helpers/Patch/PatchServiceTests.cs b/test/Altinn.App.Api.Tests/Helpers/Patch/PatchServiceTests.cs index 149329d9d..5b2614d9b 100644 --- a/test/Altinn.App.Api.Tests/Helpers/Patch/PatchServiceTests.cs +++ b/test/Altinn.App.Api.Tests/Helpers/Patch/PatchServiceTests.cs @@ -147,7 +147,12 @@ public PatchServiceTests() TaskId = "Task_1", }; - private static readonly DataElement _dataElement = new() { Id = _dataGuid.ToString(), DataType = _dataType.Id }; + private static readonly DataElement _dataElement = new() + { + Id = _dataGuid.ToString(), + DataType = _dataType.Id, + ContentType = "application/xml", + }; public class MyModel { @@ -322,8 +327,6 @@ private void SetupDataClient(MyModel oldModel) _dataClientMock .Setup(d => d.GetDataBytes( - It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs index 508e08482..77d3e7299 100644 --- a/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs @@ -30,20 +30,7 @@ IHttpContextAccessor httpContextAccessor _modelSerialization = modelSerialization; } - 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, @@ -52,6 +39,9 @@ public async Task DeleteData( CancellationToken cancellationToken = default ) { + (string org, string app) = TestData.GetInstanceOrgApp( + new InstanceIdentifier(instanceOwnerPartyId, instanceGuid) + ); string dataElementPath = TestData.GetDataElementPath(org, app, instanceOwnerPartyId, instanceGuid, dataGuid); if (delay) @@ -86,8 +76,6 @@ public async Task DeleteData( } public async Task GetBinaryData( - string org, - string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataId, @@ -96,15 +84,7 @@ public async Task GetBinaryData( ) { return new MemoryStream( - await GetDataBytes( - org, - app, - instanceOwnerPartyId, - instanceGuid, - dataId, - authenticationMethod, - cancellationToken - ) + await GetDataBytes(instanceOwnerPartyId, instanceGuid, dataId, authenticationMethod, cancellationToken) ); } @@ -146,8 +126,6 @@ public Task GetBinaryDataStream( } public async Task GetDataBytes( - string org, - string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataId, @@ -155,20 +133,24 @@ public async Task GetDataBytes( CancellationToken cancellationToken = default ) { + (string org, string app) = TestData.GetInstanceOrgApp( + new InstanceIdentifier(instanceOwnerPartyId, instanceGuid) + ); string dataPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataId); return await File.ReadAllBytesAsync(dataPath, cancellationToken); } public async Task> GetBinaryDataList( - string org, - string app, int instanceOwnerPartyId, Guid instanceGuid, StorageAuthenticationMethod? authenticationMethod = null, CancellationToken cancellationToken = default ) { + (string org, string app) = TestData.GetInstanceOrgApp( + new InstanceIdentifier(instanceOwnerPartyId, instanceGuid) + ); var dataElements = await GetDataElements(org, app, instanceOwnerPartyId, instanceGuid, cancellationToken); List list = new(); foreach (DataElement dataElement in dataElements) @@ -192,7 +174,8 @@ public async Task> GetBinaryDataList( return list; } - public async Task GetFormData( + [Obsolete("Use the GetFormData method with Instance parameter instead")] + public Task GetFormData( Guid instanceGuid, Type type, string org, @@ -203,11 +186,19 @@ public async Task GetFormData( CancellationToken cancellationToken = default ) { - var dataElementPath = TestData.GetDataElementPath(org, app, instanceOwnerPartyId, instanceGuid, dataId); - DataElement dataElement = await InstanceClientMockSi.ReadJsonFile( - dataElementPath, - cancellationToken - ); + return Task.FromException(new NotImplementedException()); + } + + public async Task GetFormData( + Instance instance, + DataElement dataElement, + StorageAuthenticationMethod? authenticationMethod = null, + CancellationToken cancellationToken = default + ) + { + var instanceIdentifier = new InstanceIdentifier(instance); + var (org, app) = TestData.GetInstanceOrgApp(instanceIdentifier); + var application = await _appMetadata.GetApplicationMetadata(); var dataType = application.DataTypes.Find(d => d.Id == dataElement?.DataType) @@ -215,68 +206,68 @@ public async Task GetFormData( $"Data type {dataElement?.DataType} not found in applicationmetadata.json" ); - string dataPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataId); + string dataPath = TestData.GetDataBlobPath( + org, + app, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid, + Guid.Parse(dataElement.Id) + ); var dataBytes = await File.ReadAllBytesAsync(dataPath, cancellationToken); - var formData = _modelSerialization.DeserializeFromStorage(dataBytes, dataType); + var formData = _modelSerialization.DeserializeFromStorage(dataBytes, dataType, dataElement); return formData ?? throw new Exception("Unable to deserialize form data"); } - public async Task InsertFormData( - Instance instance, - string dataTypeString, + [Obsolete("Use the InsertFormData method with Instance parameter instead")] + public Task InsertFormData( T dataToSerialize, + Guid instanceGuid, Type type, + string org, + string app, + int instanceOwnerPartyId, + string dataType, StorageAuthenticationMethod? authenticationMethod = null, CancellationToken cancellationToken = default ) where T : notnull { - 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, - dataTypeString, - authenticationMethod, - cancellationToken - ); + return Task.FromException(new NotImplementedException()); } - public async Task InsertFormData( - T dataToSerialize, - Guid instanceGuid, - Type type, - string org, - string app, - int instanceOwnerPartyId, - string dataTypeString, + public async Task InsertFormData( + Instance instance, + string dataTypeId, + object dataToSerialize, StorageAuthenticationMethod? authenticationMethod = null, CancellationToken cancellationToken = default ) - where T : notnull { var application = await _appMetadata.GetApplicationMetadata(); var dataType = - application.DataTypes.Find(d => d.Id == dataTypeString) - ?? throw new InvalidOperationException($"Data type {dataTypeString} not found in applicationmetadata.json"); + application.DataTypes.Find(d => d.Id == dataTypeId) + ?? throw new InvalidOperationException($"Data type {dataTypeId} not found in applicationmetadata.json"); + var instanceIdentifier = new InstanceIdentifier(instance); Guid dataGuid = Guid.NewGuid(); - string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerPartyId, instanceGuid); - var (serializedBytes, contentType) = _modelSerialization.SerializeToStorage(dataToSerialize, dataType); + + string org = instance.Org; + string app = instance.AppId.Split("/")[1]; + + string dataPath = TestData.GetDataDirectory( + org, + app, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid + ); + var (serializedBytes, contentType) = _modelSerialization.SerializeToStorage(dataToSerialize, dataType, null); DataElement dataElement = new() { Id = dataGuid.ToString(), - InstanceGuid = instanceGuid.ToString(), - DataType = dataTypeString, + InstanceGuid = instanceIdentifier.InstanceGuid.ToString(), + DataType = dataTypeId, ContentType = contentType, }; @@ -288,32 +279,45 @@ await File.WriteAllBytesAsync( cancellationToken ); - await WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId, cancellationToken); + await WriteDataElementToFile(dataElement, org, app, instanceIdentifier.InstanceOwnerPartyId, cancellationToken); return dataElement; } - public async Task UpdateData( + [Obsolete("Use the UpdateFormData method with Instance parameter instead")] + public Task UpdateData( T dataToSerialize, Guid instanceGuid, Type type, string org, string app, int instanceOwnerPartyId, - Guid dataGuid, + Guid dataId, StorageAuthenticationMethod? authenticationMethod = null, CancellationToken cancellationToken = default ) where T : notnull { - ArgumentNullException.ThrowIfNull(dataToSerialize); - string dataPath = TestData.GetDataDirectory(org, app, instanceOwnerPartyId, instanceGuid); + throw new NotImplementedException(); + } - DataElement dataElement = - await GetDataElement(org, app, instanceOwnerPartyId, instanceGuid, dataGuid.ToString(), cancellationToken) - ?? throw new Exception( - $"Unable to find data element for org: {org}/{app} party: {instanceOwnerPartyId} instance: {instanceGuid} data: {dataGuid}" - ); + public async Task UpdateFormData( + Instance instance, + object dataToSerialize, + DataElement dataElement, + StorageAuthenticationMethod? authenticationMethod = null, + CancellationToken cancellationToken = default + ) + { + ArgumentNullException.ThrowIfNull(dataToSerialize); + InstanceIdentifier instanceIdentifier = new(instance); + var (org, app) = TestData.GetInstanceOrgApp(instanceIdentifier); + string dataPath = TestData.GetDataDirectory( + org, + app, + instanceIdentifier.InstanceOwnerPartyId, + instanceIdentifier.InstanceGuid + ); var application = await _appMetadata.GetApplicationMetadata(); var dataType = @@ -322,25 +326,30 @@ await GetDataElement(org, app, instanceOwnerPartyId, instanceGuid, dataGuid.ToSt $"Data type {dataElement.DataType} not found in applicationmetadata.json" ); - Directory.CreateDirectory(dataPath + @"blob"); + Directory.CreateDirectory(Path.Join(dataPath, "blob")); - var (serializedBytes, contentType) = _modelSerialization.SerializeToStorage(dataToSerialize, dataType); + var (serializedBytes, contentType) = _modelSerialization.SerializeToStorage( + dataToSerialize, + dataType, + dataElement + ); Debug.Assert(contentType == dataElement.ContentType, "Content type should not change when updating data"); await File.WriteAllBytesAsync( - Path.Join(dataPath, "blob", dataGuid.ToString()), + Path.Join(dataPath, "blob", dataElement.Id), serializedBytes.ToArray(), cancellationToken ); dataElement.LastChanged = DateTime.UtcNow; dataElement.Size = serializedBytes.Length; - await WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId, cancellationToken); + await WriteDataElementToFile(dataElement, org, app, instanceIdentifier.InstanceOwnerPartyId, cancellationToken); return dataElement; } - public async Task InsertBinaryData( + [Obsolete("The overload that takes a HttpRequest is deprecated, use the overload that takes a Stream instead")] + public Task InsertBinaryData( string org, string app, int instanceOwnerPartyId, @@ -351,38 +360,7 @@ public async Task InsertBinaryData( CancellationToken cancellationToken = default ) { - 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(Path.Join(dataPath, "blob")); - - using var stream = new MemoryStream(); - await request.Body.CopyToAsync(stream, cancellationToken); - - var fileData = stream.ToArray(); - await File.WriteAllBytesAsync(Path.Join(dataPath, "blob", dataGuid.ToString()), fileData, cancellationToken); - - dataElement.Size = fileData.Length; - - await WriteDataElementToFile(dataElement, org, app, instanceOwnerPartyId, cancellationToken); - - return dataElement; + return Task.FromException(new NotImplementedException()); } public async Task UpdateBinaryData( @@ -476,7 +454,7 @@ public async Task InsertBinaryData( Directory.CreateDirectory(directory); } - Directory.CreateDirectory(dataPath + @"blob"); + Directory.CreateDirectory(Path.Join(dataPath, "blob")); using var memoryStream = new MemoryStream(); stream.Seek(0, SeekOrigin.Begin); diff --git a/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs index 7d188dc32..18c8ef181 100644 --- a/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs +++ b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs @@ -10,7 +10,7 @@ namespace Altinn.App.Api.Tests.Mocks; -public class InstanceClientMockSi : IInstanceClient +public sealed class InstanceClientMockSi : IInstanceClient { private readonly ILogger _logger; private readonly IHttpContextAccessor _httpContextAccessor; @@ -33,21 +33,21 @@ public InstanceClientMockSi(ILogger logger, IHttpContextAccesso _httpContextAccessor = httpContextAccessor; } - public async Task CreateInstance(string org, string app, Instance instance) + public async Task CreateInstance(string org, string app, Instance instanceTemplate) { - string partyId = instance.InstanceOwner.PartyId; + string partyId = instanceTemplate.InstanceOwner.PartyId; Guid instanceGuid = Guid.NewGuid(); - instance.Id = $"{partyId}/{instanceGuid}"; - instance.AppId = $"{org}/{app}"; - instance.Org = org; - instance.Data = new List(); + instanceTemplate.Id = $"{partyId}/{instanceGuid}"; + instanceTemplate.AppId = $"{org}/{app}"; + instanceTemplate.Org = org; + instanceTemplate.Data = new List(); string instancePath = GetInstancePath(app, org, int.Parse(partyId), instanceGuid); string directory = Path.GetDirectoryName(instancePath) ?? throw new IOException($"Could not get directory name of specified path {instancePath}"); _ = Directory.CreateDirectory(directory); - await WriteJsonFile(instancePath, instance); + await WriteJsonFile(instancePath, instanceTemplate); _logger.LogInformation( "Created instance for app {org}/{app}. writing to path: {instancePath}", @@ -56,7 +56,7 @@ public async Task CreateInstance(string org, string app, Instance inst instancePath ); - return instance; + return instanceTemplate; } /// @@ -220,10 +220,7 @@ public async Task AddCompleteConfirmation(int instanceOwnerPartyId, Gu break; } - instance.CompleteConfirmations = new List - { - new CompleteConfirmation { StakeholderId = org }, - }; + instance.CompleteConfirmations = [new CompleteConfirmation { StakeholderId = org }]; return instance; } @@ -397,8 +394,8 @@ public async Task DeleteInstance(int instanceOwnerPartyId, Guid instan /// public async Task> GetInstances(Dictionary queryParams) { - List validQueryParams = new() - { + List validQueryParams = + [ "org", "appId", "process.currentTask", @@ -420,7 +417,7 @@ public async Task> GetInstances(Dictionary "status.isActiveorSoftDeleted", "sortBy", "archiveReference", - }; + ]; string invalidKey = queryParams.FirstOrDefault(q => !validQueryParams.Contains(q.Key)).Key; if (!string.IsNullOrEmpty(invalidKey)) @@ -489,9 +486,9 @@ public async Task> GetInstances(Dictionary instances.RemoveAll(i => !i.AppId.Equals(appId, StringComparison.OrdinalIgnoreCase)); } - if (queryParams.ContainsKey("instanceOwner.partyId")) + if (queryParams.TryGetValue("instanceOwner.partyId", out var instanceOwnerPartyIdParam)) { - instances.RemoveAll(i => !queryParams["instanceOwner.partyId"].Contains(i.InstanceOwner.PartyId)); + instances.RemoveAll(i => !instanceOwnerPartyIdParam.Contains(i.InstanceOwner.PartyId)); } if (queryParams.ContainsKey("archiveReference")) @@ -500,11 +497,9 @@ public async Task> GetInstances(Dictionary instances.RemoveAll(i => !i.Id.EndsWith(archiveRef.ToLower())); } - bool match; - if ( queryParams.ContainsKey("status.isArchived") - && bool.TryParse(queryParams.GetValueOrDefault("status.isArchived"), out match) + && bool.TryParse(queryParams.GetValueOrDefault("status.isArchived"), out var match) ) { instances.RemoveAll(i => i.Status.IsArchived != match); @@ -551,16 +546,14 @@ private static (string LastChangedBy, DateTime? LastChanged) FindLastChanged(Ins } lastChanged = instance.LastChanged; - newerDataElements.ForEach( - (DataElement dataElement) => + newerDataElements.ForEach(dataElement => + { + if (dataElement.LastChanged > lastChanged) { - if (dataElement.LastChanged > lastChanged) - { - lastChangedBy = dataElement.LastChangedBy; - lastChanged = (DateTime)dataElement.LastChanged; - } + lastChangedBy = dataElement.LastChangedBy; + lastChanged = (DateTime)dataElement.LastChanged; } - ); + }); return (lastChangedBy, lastChanged); } diff --git a/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index dd89b3c3c..3d74aaaf8 100644 --- a/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -275,7 +275,6 @@ namespace Altinn.App.Api.Controllers Altinn.App.Core.Internal.Instances.IInstanceClient instanceClient, Altinn.App.Core.Internal.Data.IDataClient dataClient, Altinn.App.Core.Internal.App.IAppMetadata appMetadata, - Altinn.App.Core.Internal.AppModel.IAppModel appModel, Altinn.App.Core.Features.Auth.IAuthenticationContext authenticationContext, Altinn.Common.PEP.Interfaces.IPDP pdp, Altinn.App.Core.Internal.Events.IEventsClient eventsClient, diff --git a/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs index 72a33e394..f0f37665b 100644 --- a/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Eformidling/Implementation/DefaultEFormidlingServiceTests.cs @@ -184,8 +184,6 @@ private Fixture CreateFixture( dataClient .Setup(x => x.GetBinaryData( - It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs b/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs index 77e7e2e1b..d1b2e346f 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs @@ -179,8 +179,6 @@ public async Task AuthorizeAction_returns_true_if_other_user_has_signed_previous _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"), @@ -236,8 +234,6 @@ public async Task AuthorizeAction_returns_false_if_same_user_has_signed_previous _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"), @@ -337,8 +333,6 @@ public async Task AuthorizeAction_returns_true_if_dataelement_not_of_type_SignDo _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"), @@ -397,8 +391,6 @@ public async Task AuthorizeAction_returns_true_if_signdumcument_is_missing_signe _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"), @@ -457,8 +449,6 @@ public async Task AuthorizeAction_returns_true_if_signdumcument_is_missing_signe _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"), @@ -517,8 +507,6 @@ public async Task AuthorizeAction_returns_true_if_signdumcument_signee_userid_is _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"), @@ -555,8 +543,6 @@ private UniqueSignatureAuthorizer CreateUniqueSignatureAuthorizer( _dataClientMock .Setup(d => d.GetBinaryData( - It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2MetadataApiClientHttpMessageHandlerMoq.cs b/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2MetadataApiClientHttpMessageHandlerMoq.cs index 1b832e9c5..c5963a953 100644 --- a/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2MetadataApiClientHttpMessageHandlerMoq.cs +++ b/test/Altinn.App.Core.Tests/Features/Options/Altinn2Provider/Altinn2MetadataApiClientHttpMessageHandlerMoq.cs @@ -8,13 +8,13 @@ public class Altinn2MetadataApiClientHttpMessageHandlerMoq : HttpMessageHandler public int CallCounter { get; private set; } = 0; protected override Task SendAsync( - HttpRequestMessage httpRequestMessage, + HttpRequestMessage request, CancellationToken cancellationToken ) { CallCounter++; - var url = httpRequestMessage.RequestUri?.ToString() ?? string.Empty; + var url = request.RequestUri?.ToString() ?? string.Empty; if (url.StartsWith("https://www.altinn.no/api/metadata/codelists/serverError")) { diff --git a/test/Altinn.App.Core.Tests/Features/Signing/SigningReceiptServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Signing/SigningReceiptServiceTests.cs index 1d2853be7..ab73d348b 100644 --- a/test/Altinn.App.Core.Tests/Features/Signing/SigningReceiptServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Signing/SigningReceiptServiceTests.cs @@ -130,8 +130,6 @@ public async Task SendSignatureReceipt_CallsCorrespondenceClientWithCorrectParam dataClientMock .Setup(x => x.GetDataBytes( - It.Is(org => org == applicationMetadata.AppIdentifier.Org), - It.Is(app => app == applicationMetadata.AppIdentifier.App), It.Is(party => party == instanceIdentifier.InstanceOwnerPartyId), It.Is(guid => guid == instanceIdentifier.InstanceGuid), It.Is(id => id == Guid.Parse(signedElement.Id)), @@ -410,8 +408,6 @@ public async Task GetCorrespondenceAttachments_ReturnsCorrectAttachments() dataClientMock .Setup(x => x.GetDataBytes( - It.Is(org => org == appMetadata.AppIdentifier.Org), - It.Is(app => app == appMetadata.AppIdentifier.App), It.Is(party => party == instanceIdentifier.InstanceOwnerPartyId), It.Is(guid => guid == instanceIdentifier.InstanceGuid), It.Is(id => id == Guid.Parse(signedElement.Id)), diff --git a/test/Altinn.App.Core.Tests/Features/Validators/LegacyValidationServiceTests/ValidationServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Validators/LegacyValidationServiceTests/ValidationServiceTests.cs index 17c62ca64..398bb3dfb 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/LegacyValidationServiceTests/ValidationServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Validators/LegacyValidationServiceTests/ValidationServiceTests.cs @@ -44,6 +44,7 @@ public class MyModel { Id = _defaultDataElementId.ToString(), DataType = "MyType", + ContentType = "application/xml", }; private static readonly DataType _defaultDataType = new() @@ -298,8 +299,6 @@ private void SetupDataClient(MyModel data) _dataClientMock .Setup(d => d.GetDataBytes( - DefaultOrg, - DefaultApp, DefaultPartyId, _defaultInstanceId, _defaultDataElementId, 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 71d7b9112..a8fa3f39b 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; @@ -14,6 +15,7 @@ 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.Models; using Altinn.App.Core.Tests.Infrastructure.Clients.Storage.TestData; using Altinn.App.PlatformServices.Tests.Data; @@ -34,7 +36,7 @@ public class DataClientTests public DataClientTests() { } private const string ApiStorageEndpoint = "https://local.platform.altinn.no/api/storage/"; - private static readonly ApplicationMetadata _appMetadata = new("test-org/test-app"); + private static readonly ApplicationMetadata _appMetadata = new("test-org/test-app") { DataTypes = [] }; private static readonly Authenticated _defaultAuth = TestAuthentication.GetUserAuthentication(); private static readonly TestTokens _testTokens = new( @@ -93,7 +95,6 @@ public async Task InsertBinaryData_MethodProduceValidPlatformRequest(Authenticat // Assert Assert.NotNull(actual); - Assert.NotNull(platformRequest); AssertHttpRequest( platformRequest, expectedUri, @@ -151,7 +152,6 @@ public async Task InsertBinaryData_MethodProduceValidPlatformRequest_with_genera // Assert Assert.NotNull(actual); - Assert.NotNull(platformRequest); AssertHttpRequest( platformRequest, expectedUri, @@ -216,10 +216,9 @@ public async Task GetFormData_MethodProduceValidPlatformRequest_ReturnedFormIsVa // Assert var actual = response as SkjemaWithNamespace; Assert.NotNull(actual); - Assert.NotNull(actual!.Foretakgrp8820); - Assert.NotNull(actual!.Foretakgrp8820.EnhetNavnEndringdatadef31); + Assert.NotNull(actual.Foretakgrp8820); + Assert.NotNull(actual.Foretakgrp8820.EnhetNavnEndringdatadef31); - Assert.NotNull(platformRequest); AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Get, expectedAuth: testCase?.ExpectedToken); } @@ -296,9 +295,8 @@ public async Task UpdateBinaryData_put_updated_data_and_Return_DataElement(Authe authenticationMethod: testCase?.AuthenticationMethod ); invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); AssertHttpRequest( - platformRequest!, + platformRequest, expectedUri, HttpMethod.Put, "test.json", @@ -395,16 +393,13 @@ public async Task GetBinaryData_returns_stream_of_binary_data(AuthenticationTest UriKind.RelativeOrAbsolute ); var response = await fixture.DataClient.GetBinaryData( - "ttd", - "app", instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, dataGuid, authenticationMethod: testCase?.AuthenticationMethod ); invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Get, expectedAuth: testCase?.ExpectedToken); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Get, expectedAuth: testCase?.ExpectedToken); using StreamReader streamReader = new StreamReader(response); var responseString = await streamReader.ReadToEndAsync(); @@ -436,8 +431,6 @@ public async Task GetBinaryData_returns_empty_stream_when_storage_returns_notfou UriKind.RelativeOrAbsolute ); var response = await fixture.DataClient.GetBinaryData( - "ttd", - "app", instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, dataGuid, @@ -445,8 +438,7 @@ public async Task GetBinaryData_returns_empty_stream_when_storage_returns_notfou ); response.Should().BeNull(); invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Get, expectedAuth: testCase?.ExpectedToken); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Get, expectedAuth: testCase?.ExpectedToken); } [Fact] @@ -468,8 +460,6 @@ public async Task GetBinaryData_throws_PlatformHttpException_when_server_error_r var actual = await Assert.ThrowsAsync(async () => await fixture.DataClient.GetBinaryData( - "ttd", - "app", instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, dataGuid @@ -631,8 +621,7 @@ public async Task GetBinaryDataList_returns_AttachemtList_when_DataElements_foun authenticationMethod: testCase?.AuthenticationMethod ); invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Get, expectedAuth: testCase?.ExpectedToken); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Get, expectedAuth: testCase?.ExpectedToken); var expectedList = new List() { @@ -716,16 +705,14 @@ public async Task DeleteBinaryData_returns_true_when_data_was_deleted() $"{ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}?delay=False", UriKind.RelativeOrAbsolute ); - var result = await fixture.DataClient.DeleteBinaryData( - "ttd", - "app", + var result = await fixture.DataClient.DeleteData( instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, - dataGuid + dataGuid, + delay: false ); invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Delete); result.Should().BeTrue(); } @@ -753,16 +740,15 @@ public async Task DeleteBinaryData_throws_PlatformHttpException_when_dataelement UriKind.RelativeOrAbsolute ); var actual = await Assert.ThrowsAsync(async () => - await fixture.DataClient.DeleteBinaryData( - "ttd", - "app", + await fixture.DataClient.DeleteData( instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, - dataGuid + dataGuid, + delay: false ) ); invocations.Should().Be(1); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Delete); actual.Response.StatusCode.Should().Be(HttpStatusCode.NotFound); } @@ -800,8 +786,7 @@ public async Task DeleteData_returns_true_when_data_was_deleted_with_delay_true( authenticationMethod: testCase?.AuthenticationMethod ); invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete, expectedAuth: testCase?.ExpectedToken); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Delete, expectedAuth: testCase?.ExpectedToken); result.Should().BeTrue(); } @@ -840,9 +825,8 @@ await fixture.DataClient.UpdateData( authenticationMethod: testCase?.AuthenticationMethod ); invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); AssertHttpRequest( - platformRequest!, + platformRequest, expectedUri, HttpMethod.Put, null, @@ -877,7 +861,7 @@ await Assert.ThrowsAsync(async () => await fixture.DataClient.UpdateData( exampleModel, instanceIdentifier.InstanceGuid, - typeof(DataElement), + exampleModel.GetType(), "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, @@ -926,9 +910,8 @@ await fixture.DataClient.UpdateData( ) ); invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); AssertHttpRequest( - platformRequest!, + platformRequest, expectedUri, HttpMethod.Put, null, @@ -974,9 +957,8 @@ public async Task LockDataElement_calls_lock_endpoint_in_storage_and_returns_upd authenticationMethod: testCase?.AuthenticationMethod ); invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); response.Should().BeEquivalentTo(dataElement); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, expectedAuth: testCase?.ExpectedToken); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Put, expectedAuth: testCase?.ExpectedToken); } [Theory] @@ -1012,7 +994,7 @@ await fixture.DataClient.LockDataElement( ) ); invocations.Should().Be(1); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, expectedAuth: testCase?.ExpectedToken); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Put, expectedAuth: testCase?.ExpectedToken); result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); } @@ -1052,9 +1034,8 @@ public async Task UnlockDataElement_calls_lock_endpoint_in_storage_and_returns_u authenticationMethod: testCase?.AuthenticationMethod ); invocations.Should().Be(1); - platformRequest?.Should().NotBeNull(); response.Should().BeEquivalentTo(dataElement); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete, expectedAuth: testCase?.ExpectedToken); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Delete, expectedAuth: testCase?.ExpectedToken); } [Theory] @@ -1090,12 +1071,12 @@ await fixture.DataClient.UnlockDataElement( ) ); invocations.Should().Be(1); - AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete, expectedAuth: testCase?.ExpectedToken); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Delete, expectedAuth: testCase?.ExpectedToken); result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); } private static void AssertHttpRequest( - HttpRequestMessage actual, + [NotNull] HttpRequestMessage? actual, Uri expectedUri, HttpMethod method, string? expectedFilename = null, @@ -1103,12 +1084,13 @@ private static void AssertHttpRequest( JwtToken? expectedAuth = null ) { + Assert.NotNull(actual); Assert.Equal(method, actual.Method); var authHeader = actual.Headers.Authorization; Assert.NotNull(authHeader); Assert.Equal("Bearer", authHeader.Scheme); - Assert.Equal(authHeader.Parameter, expectedAuth ?? _defaultAuth.Token); + Assert.Equal(expectedAuth ?? _defaultAuth.Token, authHeader.Parameter); const int uriComparisonIdentical = 0; Assert.Equivalent(expectedUri, actual.RequestUri); @@ -1143,7 +1125,7 @@ private static void AssertHttpRequest( private sealed record Fixture : IAsyncDisposable { - public required DataClient DataClient { get; init; } + public required IDataClient DataClient { get; init; } public required ServiceProvider ServiceProvider { get; init; } public required FixtureMocks Mocks { get; init; } public required HttpClient BaseHttpClient { get; init; } diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTestsXmlJson.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTestsXmlJson.cs new file mode 100644 index 000000000..87dddd32e --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTestsXmlJson.cs @@ -0,0 +1,797 @@ +using System.Net; +using System.Xml.Serialization; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Helpers.Serialization; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Tests.Common.Fixtures; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Altinn.App.Core.Tests.Infrastructure.Clients.Storage; + +public class DataClientTestsXmlJson +{ + private const string Org = "org"; + private const string App = "app"; + private const int InstanceOwnerPartyId = 123456; + private static readonly Guid _instanceId = Guid.Parse("a467c267-2122-41a4-b78a-ae6f94aa7ff7"); + + private readonly MockedServiceCollection _mockedServiceCollection = new(); + private readonly Instance _instance; + + public DataClientTestsXmlJson(ITestOutputHelper outputHelper) + { + _mockedServiceCollection.AddXunitLogging(outputHelper); + _mockedServiceCollection.TryAddCommonServices(); + + _mockedServiceCollection.AddDataType( + dataTypeId: "jsonDataType", + allowedContentTypes: ["application/json"] + ); + _mockedServiceCollection.AddDataType( + dataTypeId: "xmlDataType", + allowedContentTypes: ["application/xml"] + ); + _mockedServiceCollection.AddDataType( + dataTypeId: "xmlDefaultDataType", + allowedContentTypes: ["application/xml", "application/json"] + ); + _mockedServiceCollection.AddDataType( + dataTypeId: "jsonDefaultDataType", + allowedContentTypes: ["application/json", "application/xml"] + ); + _instance = new Instance + { + Id = $"{InstanceOwnerPartyId}/{_instanceId}", + Org = Org, + AppId = $"{Org}/{App}", + InstanceOwner = new InstanceOwner { PartyId = InstanceOwnerPartyId.ToString() }, + Data = [], + }; + _mockedServiceCollection.Storage.AddInstance(_instance); + } + + public static TheoryData DataTypes => + new() + { + { "jsonDataType", "application/json" }, + { "xmlDataType", "application/xml" }, + { "xmlDefaultDataType", "application/xml" }, + { "jsonDefaultDataType", "application/json" }, + }; + + [Theory] + [MemberData(nameof(DataTypes))] + public async Task TestInsertDataAlternatives(string dataType, string requestContentType) + { + TestData data = GetDataForType(dataType); + await using var serviceProvider = _mockedServiceCollection.BuildServiceProvider(); + var dataClient = serviceProvider.GetRequiredService(); + + // New endpoint with instance + var elementNew = await dataClient.InsertFormData(_instance, dataType, data); + + Assert.Equal(dataType, elementNew.DataType); + Assert.Equal(requestContentType, elementNew.ContentType); + + // Obsolete endpoint with instance and type + var elementInstanceObsolete = await dataClient.InsertFormData(_instance, dataType, data, data.GetType()); + + Assert.Equal(dataType, elementInstanceObsolete.DataType); + Assert.Equal(requestContentType, elementInstanceObsolete.ContentType); + + // Obsolete endpoint with instance and type + var elementObsoleteType = await dataClient.InsertFormData(_instance, dataType, data, data.GetType()); + + Assert.Equal(dataType, elementObsoleteType.DataType); + Assert.Equal(requestContentType, elementObsoleteType.ContentType); + + // Obsolete endpoint without instance + var elementObsolete = await dataClient.InsertFormData( + data, + _instanceId, + data.GetType(), + Org, + App, + InstanceOwnerPartyId, + dataType + ); + Assert.Equal(dataType, elementObsolete.DataType); + Assert.Equal(requestContentType, elementObsolete.ContentType); + + // Verify that both requests were made correctly + Assert.Equal(4, _mockedServiceCollection.Storage.RequestsResponses.Count); + + // Verify content of requests + Assert.All( + _mockedServiceCollection.Storage.RequestsResponses, + requestResponse => + { + Assert.Equal(HttpMethod.Post, requestResponse.RequestMethod); + Assert.Equal( + $"/storage/api/v1/instances/{InstanceOwnerPartyId}/{_instanceId}/data?dataType={dataType}", + requestResponse.RequestUrl?.PathAndQuery + ); + Assert.Equal( + requestContentType, + requestResponse.RequestContentHeaders?.GetValues("Content-Type").Single() + ); + if (requestContentType == "application/json") + { + Assert.Equal("""{"name":"ivar","age":36}""", requestResponse.RequestBody); + } + else if (requestContentType == "application/xml") + { + Assert.Equal( + """ + + + ivar + 36 + + """.Replace("\r\n", "", StringComparison.Ordinal).Replace(" ", "", StringComparison.Ordinal), + requestResponse.RequestBody + ); + } + } + ); + + _mockedServiceCollection.VerifyMocks(); + } + + [Fact] + public async Task TestObsoleteInsertFormData_TestTypeMismatch() + { + await using var serviceProvider = _mockedServiceCollection.BuildServiceProvider(); + var dataClient = serviceProvider.GetRequiredService(); + var act = async () => + await dataClient.InsertFormData( + _instance, + "jsonDataType", + new TestDataXml { Name = "ivar", Age = 36 }, + typeof(TestDataJson) + ); + var exception = await Assert.ThrowsAsync(act); + Assert.Equal( + $"The provided type {typeof(TestDataJson).FullName} does not match the type of dataToSerialize {typeof(TestDataXml).FullName}", + exception.Message + ); + + var act2 = async () => + await dataClient.InsertFormData( + new TestDataXml { Name = "ivar", Age = 36 }, + _instanceId, + typeof(TestDataJson), + Org, + App, + InstanceOwnerPartyId, + "jsonDataType" + ); + var exception2 = await Assert.ThrowsAsync(act2); + Assert.Equal( + $"The provided type {typeof(TestDataJson).FullName} does not match the type of dataToSerialize {typeof(TestDataXml).FullName}", + exception2.Message + ); + + Assert.Empty(_mockedServiceCollection.Storage.RequestsResponses); + } + + [Theory] + [MemberData(nameof(DataTypes))] + public async Task TestUpdateDataAlternatives(string dataType, string requestContentType) + { + TestData data = GetDataForType(dataType); + var oldElement = _mockedServiceCollection.Storage.AddData(_instance, dataType, requestContentType, []); + + await using var serviceProvider = _mockedServiceCollection.BuildServiceProvider(); + var dataClient = serviceProvider.GetRequiredService(); + + var actObsolete = async () => + await dataClient.UpdateData( + data, + _instanceId, + data.GetType(), + Org, + App, + InstanceOwnerPartyId, + Guid.Parse(oldElement.Id) + ); + + var actObsoleteGeneric = async () => + await dataClient.UpdateData( + data, + _instanceId, + data.GetType(), + Org, + App, + InstanceOwnerPartyId, + Guid.Parse(oldElement.Id) + ); + + var actNew = async () => await dataClient.UpdateFormData(_instance, data, oldElement); + + if (dataType == "xmlDataType") + { + var newElement = await actObsolete(); + Assert.Equal(dataType, newElement.DataType); + Assert.Equal(requestContentType, newElement.ContentType); + Assert.Equal(oldElement.Id, newElement.Id); + var obsoleteGenericElement = await actObsoleteGeneric(); + Assert.Equal(dataType, obsoleteGenericElement.DataType); + Assert.Equal(requestContentType, obsoleteGenericElement.ContentType); + Assert.Equal(oldElement.Id, obsoleteGenericElement.Id); + var updatedElement = await actNew(); + Assert.Equal(dataType, updatedElement.DataType); + Assert.Equal(requestContentType, updatedElement.ContentType); + Assert.Equal(oldElement.Id, updatedElement.Id); + + Assert.All( + _mockedServiceCollection.Storage.RequestsResponses, + requestResponse => + { + Assert.Equal(HttpMethod.Put, requestResponse.RequestMethod); + Assert.Equal( + $"/storage/api/v1/instances/{InstanceOwnerPartyId}/{_instanceId}/data/{oldElement.Id}", + requestResponse.RequestUrl?.PathAndQuery + ); + Assert.Equal( + requestContentType, + requestResponse.RequestContentHeaders?.GetValues("Content-Type").Single() + ); + + Assert.Equal( + """ + + + ivar + 36 + + """.Replace("\r\n", "", StringComparison.Ordinal).Replace(" ", "", StringComparison.Ordinal), + requestResponse.RequestBody + ); + } + ); + _mockedServiceCollection.VerifyMocks(); + } + else + { + var obsoleteException = await Assert.ThrowsAsync(actObsolete); + Assert.Equal( + $"The data type {data.GetType().FullName} is configured to use JSON serialization and must use UpdateFormData method instead", + obsoleteException.Message + ); + Assert.Empty(_mockedServiceCollection.Storage.RequestsResponses); + + var obsoleteGenericException = await Assert.ThrowsAsync(actObsoleteGeneric); + Assert.Equal( + $"The data type {data.GetType().FullName} is configured to use JSON serialization and must use UpdateFormData method instead", + obsoleteGenericException.Message + ); + Assert.Empty(_mockedServiceCollection.Storage.RequestsResponses); + + var updatedElement = await actNew(); + Assert.Equal(dataType, updatedElement.DataType); + Assert.Equal(requestContentType, updatedElement.ContentType); + Assert.Equal(oldElement.Id, updatedElement.Id); + var request = Assert.Single(_mockedServiceCollection.Storage.RequestsResponses); + Assert.Equal(HttpMethod.Put, request.RequestMethod); + Assert.Equal( + $"/storage/api/v1/instances/{InstanceOwnerPartyId}/{_instanceId}/data/{oldElement.Id}", + request.RequestUrl?.PathAndQuery + ); + Assert.Equal(requestContentType, request.RequestContentHeaders?.GetValues("Content-Type").Single()); + if (requestContentType == "application/json") + { + Assert.Equal("""{"name":"ivar","age":36}""", request.RequestBody); + } + else if (requestContentType == "application/xml") + { + Assert.Equal( + """ + + + ivar + 36 + + """.Replace("\r\n", "", StringComparison.Ordinal).Replace(" ", "", StringComparison.Ordinal), + request.RequestBody + ); + } + } + } + + [Fact] + public async Task TestObsoleteUpdateData_TestTypeMismatch() + { + var oldElement = _mockedServiceCollection.Storage.AddData(_instance, "jsonDataType", "application/json", []); + + await using var serviceProvider = _mockedServiceCollection.BuildServiceProvider(); + var dataClient = serviceProvider.GetRequiredService(); + var act = async () => + await dataClient.UpdateData( + new TestDataXml { Name = "ivar", Age = 36 }, + _instanceId, + typeof(TestDataJson), + Org, + App, + InstanceOwnerPartyId, + Guid.Parse(oldElement.Id) + ); + var exception = await Assert.ThrowsAsync(act); + Assert.Equal( + $"The provided type {typeof(TestDataJson).FullName} does not match the type of dataToSerialize {typeof(TestDataXml).FullName}", + exception.Message + ); + + Assert.Empty(_mockedServiceCollection.Storage.RequestsResponses); + } + + [Theory] + [MemberData(nameof(DataTypes))] + public async Task TestGetFormData(string dataType, string storedContentType) + { + TestData data = GetDataForType(dataType); + await using var serviceProvider = _mockedServiceCollection.BuildServiceProvider(); + var serializationService = serviceProvider.GetRequiredService(); + _mockedServiceCollection.Storage.AddData( + _instance, + dataType, + storedContentType, + storedContentType switch + { + "application/json" => serializationService.SerializeToJson(data).ToArray(), + "application/xml" => serializationService.SerializeToXml(data).ToArray(), + _ => throw new NotSupportedException($"Content type {storedContentType} not supported"), + } + ); + var element = _instance.Data.First(d => d.DataType == dataType); + Assert.NotNull(element); + + var dataClient = serviceProvider.GetRequiredService(); + + // Verify Most up-to-date method + var retrievedData = await dataClient.GetFormData(_instance, element); + Assert.Equivalent(data, retrievedData); + + // Verify Obsolete method + async Task RetrievedDataObsolete() => + await dataClient.GetFormData( + _instanceId, + data.GetType(), + Org, + App, + InstanceOwnerPartyId, + Guid.Parse(element.Id) + ); + if (dataType == "xmlDataType") + { + var result = await RetrievedDataObsolete(); + Assert.Equivalent(data, result); + } + else + { + var exception = await Assert.ThrowsAsync(RetrievedDataObsolete); + Assert.Equal( + $"The data type {data.GetType().FullName} is configured to use JSON serialization and must use GetFormData with dataElement argument instead", + exception.Message + ); + } + + _mockedServiceCollection.VerifyMocks(); + } + + [Fact] + public async Task IDataClientGet_ThrowsPlatformException() + { + var wrongElementId = Guid.NewGuid(); + var wrongInstanceId = Guid.NewGuid(); + var wrongDataElement = new DataElement() + { + Id = wrongElementId.ToString(), + InstanceGuid = wrongInstanceId.ToString(), + DataType = "jsonDataType", + ContentType = "application/json", + }; + await using var serviceProvider = _mockedServiceCollection.BuildServiceProvider(); + var dataClient = serviceProvider.GetRequiredService(); + + // Test GetFromData method with non existing data id + await Assert.ThrowsAsync(() => + dataClient.GetFormData(_instanceId, typeof(TestDataXml), Org, App, InstanceOwnerPartyId, wrongElementId) + ); + await Assert.ThrowsAsync(() => dataClient.GetFormData(_instance, wrongDataElement)); + + await Assert.ThrowsAsync(() => + dataClient.GetFormData(_instance, wrongDataElement) + ); + + Assert.All( + _mockedServiceCollection.Storage.RequestsResponses, + requestResponse => + { + Assert.Equal(HttpStatusCode.NotFound, requestResponse.ResponseStatusCode); + } + ); + } + + [Fact] + public async Task IDataClientUpdate_ThrowsPlatformException() + { + var wrongElementId = Guid.NewGuid(); + var wrongInstanceId = Guid.NewGuid(); + await using var serviceProvider = _mockedServiceCollection.BuildServiceProvider(); + var dataClient = serviceProvider.GetRequiredService(); + await Assert.ThrowsAsync(() => + dataClient.UpdateData( + new TestDataXml { Name = "ivar", Age = 36 }, + _instanceId, + typeof(TestDataXml), + Org, + App, + InstanceOwnerPartyId, + wrongElementId + ) + ); + await Assert.ThrowsAsync(() => + dataClient.InsertFormData( + new TestDataJson { Name = "ivar", Age = 36 }, + wrongInstanceId, + typeof(TestDataJson), + Org, + App, + InstanceOwnerPartyId, + "jsonDataType" + ) + ); + + await Assert.ThrowsAsync(() => + dataClient.GetDataBytes(Org, App, InstanceOwnerPartyId, _instanceId, wrongElementId) + ); + + _mockedServiceCollection.VerifyMocks(); + } + + // [Theory] + // [MemberData(nameof(DataTypes))] + // public async Task TestUpdateFormData(string dataType, string requestContentType) + // { + // var dataElement = new DataElement + // { + // Id = _dataGuid.ToString(), + // InstanceGuid = _instanceId.ToString(), + // ContentType = requestContentType, + // DataType = dataType, + // }; + // var instance = new Instance + // { + // Id = $"{InstanceOwnerPartyId}/{_instanceId}", + // InstanceOwner = new InstanceOwner { PartyId = InstanceOwnerPartyId.ToString() }, + // Data = [dataElement], + // }; + + // _fakeResponses.Add( + // new FakeResponse( + // HttpMethod.Put, + // $"{ApiStorageEndpoint}instances/{InstanceOwnerPartyId}/{_instanceId}/data/{_dataGuid}", + // requestContentType, + // HttpStatusCode.OK, + // "application/json", + // JsonSerializer.Serialize(dataElement) + // ) + // ); + // // The tests share the same ClassRef, and the compatibility check will fail if any type supports json + // _appMetadata.DataTypes.RemoveAll(d => d.Id != dataType); + + // await using var serviceProvider = _services.BuildServiceProvider(); + // var dataClient = serviceProvider.GetRequiredService(); + // var element = await dataClient.UpdateFormData(instance, new TestData { Name = "ivar", Age = 36 }, dataElement); + + // await VerifyMocks(element, dataType); + // } + + // [Theory] + // [MemberData(nameof(DataTypes))] + // public async Task TestObsoleteGetFormData(string dataType, string storedContentType) + // { + // var storedContent = storedContentType switch + // { + // "application/json" => """{"Name":"ivar","Age":36}""", + // "application/xml" => """ + // + // + // ivar + // 36 + // + // """, + // _ => throw new NotSupportedException($"Content type {storedContentType} not supported"), + // }; + // _fakeResponses.Add( + // new FakeResponse( + // HttpMethod.Get, + // $"{ApiStorageEndpoint}instances/{InstanceOwnerPartyId}/{_instanceId}/data/{_dataGuid}", + // null, + // HttpStatusCode.OK, + // storedContentType, + // storedContent + // ) + // ); + // // The tests share the same ClassRef, and the compatibility check will fail if any type supports json + // _appMetadata.DataTypes.RemoveAll(d => d.Id != dataType); + + // await using var serviceProvider = _services.BuildServiceProvider(); + // var dataClient = serviceProvider.GetRequiredService(); + // var act = async () => + // await dataClient.GetFormData(_instanceId, typeof(TestData), Org, App, InstanceOwnerPartyId, _dataGuid); + // if (dataType == "xmlDataType") + // { + // var data = await act(); + // var typedData = Assert.IsType(data); + // Assert.Equal("ivar", typedData.Name); + // Assert.Equal(36, typedData.Age); + // await VerifyMocks(data, dataType); + // } + // else + // { + // await Assert.ThrowsAsync(act); + // } + // } + + // [Theory] + // [MemberData(nameof(DataTypes))] + // public async Task TestGetFormData(string dataType, string storedContentType) + // { + // var dataElement = new DataElement() + // { + // Id = _dataGuid.ToString(), + // InstanceGuid = _instanceId.ToString(), + // ContentType = storedContentType, + // DataType = dataType, + // }; + // var instance = new Instance() + // { + // Id = $"{InstanceOwnerPartyId}/{_instanceId}", + // InstanceOwner = new InstanceOwner { PartyId = InstanceOwnerPartyId.ToString() }, + // Data = [dataElement], + // }; + // var storedContent = storedContentType switch + // { + // "application/json" => """{"Name":"ivar","Age":36}""", + // "application/xml" => """ + // + // + // ivar + // 36 + // + // """, + // _ => throw new NotSupportedException($"Content type {storedContentType} not supported"), + // }; + // _fakeResponses.Add( + // new FakeResponse( + // HttpMethod.Get, + // $"{ApiStorageEndpoint}instances/{InstanceOwnerPartyId}/{_instanceId}/data/{_dataGuid}", + // null, + // HttpStatusCode.OK, + // storedContentType, + // storedContent + // ) + // ); + // // The tests share the same ClassRef, and the compatibility check will fail if any type supports json + // _appMetadata.DataTypes.RemoveAll(d => d.Id != dataType); + + // await using var serviceProvider = _services.BuildServiceProvider(); + // var dataClient = serviceProvider.GetRequiredService(); + // var data = await dataClient.GetFormData(instance, dataElement); + // var typedData = Assert.IsType(data); + // Assert.Equal("ivar", typedData.Name); + // Assert.Equal(36, typedData.Age); + // await VerifyMocks(data, dataType); + // } + + // [Theory] + // [MemberData(nameof(DataTypes))] + // public async Task TestGetFormDataExtension(string dataType, string storedContentType) + // { + // var dataElement = new DataElement() + // { + // Id = _dataGuid.ToString(), + // InstanceGuid = _instanceId.ToString(), + // ContentType = storedContentType, + // DataType = dataType, + // }; + // var instance = new Instance() + // { + // Id = $"{InstanceOwnerPartyId}/{_instanceId}", + // InstanceOwner = new InstanceOwner { PartyId = InstanceOwnerPartyId.ToString() }, + // Data = [dataElement], + // }; + // var storedContent = storedContentType switch + // { + // "application/json" => """{"Name":"ivar","Age":36}""", + // "application/xml" => """ + // + // + // ivar + // 36 + // + // """, + // _ => throw new NotSupportedException($"Content type {storedContentType} not supported"), + // }; + // _fakeResponses.Add( + // new FakeResponse( + // HttpMethod.Get, + // $"{ApiStorageEndpoint}instances/{InstanceOwnerPartyId}/{_instanceId}/data/{_dataGuid}", + // null, + // HttpStatusCode.OK, + // storedContentType, + // storedContent + // ) + // ); + // // The tests share the same ClassRef, and the compatibility check will fail if any type supports json + // _appMetadata.DataTypes.RemoveAll(d => d.Id != dataType); + + // await using var serviceProvider = _services.BuildServiceProvider(); + // var dataClient = serviceProvider.GetRequiredService(); + // TestData typedData = await dataClient.GetFormData(instance, dataElement); + // Assert.Equal("ivar", typedData.Name); + // Assert.Equal(36, typedData.Age); + + // // Get code coverage on invalid cast + // await Assert.ThrowsAsync(async () => + // await dataClient.GetFormData(instance, dataElement) + // ); + // await VerifyMocks(typedData, dataType); + // } + + // [Fact] + // public async Task TestGetBinaryData_AllVariants() + // { + // var content = "binary-content"; + // _fakeResponses.Add( + // new( + // HttpMethod.Get, + // $"{ApiStorageEndpoint}instances/{InstanceOwnerPartyId}/{_instanceId}/data/{_dataGuid}", + // null, + // HttpStatusCode.OK, + // "application/octet-stream", + // content + // ) + // ); + // await using var serviceProvider = _services.BuildServiceProvider(); + // var dataClient = serviceProvider.GetRequiredService(); + + // // Test Obsolete byte[] method signature + // var obsoleteByteArray = await dataClient.GetDataBytes(Org, App, InstanceOwnerPartyId, _instanceId, _dataGuid); + // var obsoleteByteArrayContent = Encoding.UTF8.GetString(obsoleteByteArray); + // Assert.Equal(content, obsoleteByteArrayContent); + // // Test current byte[] method signature + // var byteArray = await dataClient.GetDataBytes(InstanceOwnerPartyId, _instanceId, _dataGuid); + // var byteArrayContent = Encoding.UTF8.GetString(byteArray); + // Assert.Equal(content, byteArrayContent); + // // Test obsolete method signature + // using (var stream = await dataClient.GetBinaryData(Org, App, InstanceOwnerPartyId, _instanceId, _dataGuid)) + // { + // var bytes = new byte[stream.Length]; + // await stream.ReadExactlyAsync(bytes, 0, bytes.Length); + // var resultContent = Encoding.UTF8.GetString(bytes); + // Assert.Equal(content, resultContent); + // } + // // Test current method signature + // using (var stream = await dataClient.GetBinaryData(InstanceOwnerPartyId, _instanceId, _dataGuid)) + // { + // var bytes = new byte[stream.Length]; + // await stream.ReadExactlyAsync(bytes, 0, bytes.Length); + // var resultContent = Encoding.UTF8.GetString(bytes); + // Assert.Equal(content, resultContent); + // await VerifyMocks(resultContent, "binary"); + // } + // } + + // [Fact] + // public async Task TestGetBinaryData_TeaPotResult() + // { + // var content = "binary-content"; + // _fakeResponses.Add( + // new( + // HttpMethod.Get, + // $"{ApiStorageEndpoint}instances/{InstanceOwnerPartyId}/{_instanceId}/data/{_dataGuid}", + // null, + // HttpStatusCode.Unused, + // "application/octet-stream", + // content + // ) + // ); + // await using var serviceProvider = _services.BuildServiceProvider(); + // var dataClient = serviceProvider.GetRequiredService(); + + // // Test obsolete method signature + // var exception = await Assert.ThrowsAsync(() => + // dataClient.GetBinaryData(Org, App, InstanceOwnerPartyId, _instanceId, _dataGuid) + // ); + // Assert.Equal(HttpStatusCode.Unused, exception.Response.StatusCode); + + // // Test current method signature + // exception = await Assert.ThrowsAsync(() => + // dataClient.GetBinaryData(InstanceOwnerPartyId, _instanceId, _dataGuid) + // ); + // Assert.Equal(HttpStatusCode.Unused, exception.Response.StatusCode); + + // // Test Obsolete byte[] method signature + // exception = await Assert.ThrowsAsync(() => + // dataClient.GetDataBytes(Org, App, InstanceOwnerPartyId, _instanceId, _dataGuid) + // ); + // Assert.Equal(HttpStatusCode.Unused, exception.Response.StatusCode); + + // // Test current byte[] method signature + // exception = await Assert.ThrowsAsync(() => + // dataClient.GetDataBytes(InstanceOwnerPartyId, _instanceId, _dataGuid) + // ); + // Assert.Equal(HttpStatusCode.Unused, exception.Response.StatusCode); + + // await VerifyMocks(content, "binary-teapot"); + // } + + // [Fact] + // public async Task UpdateBinaryData() + // { + // var instanceIdentifier = new InstanceIdentifier(InstanceOwnerPartyId, _instanceId); + + // var dataElement = new DataElement + // { + // Id = _dataGuid.ToString(), + // InstanceGuid = _instanceId.ToString(), + // ContentType = "application/pdf", + // DataType = "binaryDataType", + // }; + // _fakeResponses.Add( + // new( + // HttpMethod.Put, + // $"{ApiStorageEndpoint}instances/{InstanceOwnerPartyId}/{_instanceId}/data/{_dataGuid}", + // "application/pdf", + // HttpStatusCode.OK, + // "application/json", + // JsonSerializer.Serialize(dataElement) + // ) + // ); + + // await using var serviceProvider = _services.BuildServiceProvider(); + // var dataClient = serviceProvider.GetRequiredService(); + // var result = await dataClient.UpdateBinaryData( + // instanceIdentifier, + // "application/pdf", + // "filename", + // _dataGuid, + // new MemoryStream(Encoding.UTF8.GetBytes("binary-content")) + // ); + + // await VerifyMocks(result, "binary-teapot"); + // } + + public class TestData + { + public string? Name { get; set; } + public int? Age { get; set; } + }; + + public class TestDataJson : TestData { } + + [XmlRoot("TestData")] + public class TestDataXml : TestData { } + + public class TestDataJsonXml : TestData { } + + [XmlRoot("TestData")] + public class TestDataXmlJson : TestData { } + + private static TestData GetDataForType(string dataType) + { + return dataType switch + { + "jsonDataType" => new TestDataJson { Name = "ivar", Age = 36 }, + "xmlDataType" => new TestDataXml { Name = "ivar", Age = 36 }, + "xmlDefaultDataType" => new TestDataXmlJson { Name = "ivar", Age = 36 }, + "jsonDefaultDataType" => new TestDataJsonXml { Name = "ivar", Age = 36 }, + _ => throw new NotSupportedException($"Data type {dataType} not supported"), + }; + } +} diff --git a/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs index 7a4cc2500..887655944 100644 --- a/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Data/DataServiceTests.cs @@ -20,7 +20,7 @@ public DataServiceTests() { _mockDataClient = new Mock(); _mockAppMetadata = new Mock(); - _dataService = new DataService(_mockDataClient.Object, _mockAppMetadata.Object); + _dataService = new DataService(_mockDataClient.Object); } [Fact] @@ -42,8 +42,6 @@ public async Task GetByType_ReturnsCorrectDataElementAndModel_WhenDataElementExi _mockDataClient .Setup(dc => dc.GetBinaryData( - applicationMetadata.AppIdentifier.Org, - applicationMetadata.AppIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, new Guid(instance.Data.First().Id), @@ -99,8 +97,6 @@ public async Task GetById_ReturnsCorrectModel_WhenDataElementExists() _mockDataClient .Setup(dc => dc.GetBinaryData( - applicationMetadata.AppIdentifier.Org, - applicationMetadata.AppIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, expectedDataId, diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs b/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs index c81f765fb..9f8495aa7 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ExpressionsExclusiveGatewayTests.cs @@ -72,7 +72,12 @@ public async Task FilterAsync_NoExpressions_ReturnsAllFlows() Process = new() { CurrentTask = new() { ElementId = TaskId } }, Data = new() { - new() { Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", DataType = DefaultDataTypeName }, + new() + { + Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", + DataType = DefaultDataTypeName, + ContentType = "application/json", + }, }, }; var processGatewayInformation = new ProcessGatewayInformation { Action = "confirm" }; @@ -115,7 +120,12 @@ public async Task FilterAsync_Expression_filters_based_on_action() Process = new() { CurrentTask = new() { ElementId = TaskId } }, Data = new() { - new() { Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", DataType = DefaultDataTypeName }, + new() + { + Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", + DataType = DefaultDataTypeName, + ContentType = "application/json", + }, }, }; var processGatewayInformation = new ProcessGatewayInformation { Action = "confirm" }; @@ -169,7 +179,12 @@ public async Task FilterAsync_Expression_filters_based_on_datamodel_set_by_layou Process = new() { CurrentTask = new() { ElementId = TaskId } }, Data = new() { - new() { Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", DataType = DefaultDataTypeName }, + new() + { + Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", + DataType = DefaultDataTypeName, + ContentType = "application/json", + }, }, }; var processGatewayInformation = new ProcessGatewayInformation { Action = "confirm" }; @@ -227,7 +242,12 @@ public async Task FilterAsync_Expression_filters_based_on_datamodel_set_by_gatew Process = new() { CurrentTask = new() { ElementId = "Task_1" } }, Data = new() { - new() { Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", DataType = "aa" }, + new() + { + Id = "cd9204e7-9b83-41b4-b2f2-9b196b4fafcf", + DataType = "aa", + ContentType = "application/json", + }, }, }; var processGatewayInformation = new ProcessGatewayInformation { Action = "confirm", DataTypeId = "aa" }; @@ -258,8 +278,6 @@ public async Task FilterAsync_Expression_filters_based_on_datamodel_set_by_gatew _dataClient .Setup(d => d.GetDataBytes( - It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), @@ -267,7 +285,7 @@ public async Task FilterAsync_Expression_filters_based_on_datamodel_set_by_gatew It.IsAny() ) ) - .ReturnsAsync(modelSerializationService.SerializeToXml(formData).ToArray()); + .ReturnsAsync(modelSerializationService.SerializeToJson(formData).ToArray()); _appModel.Setup(am => am.GetModelType(_classRef)).Returns(formData.GetType()); } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.CleanFull.verified.txt b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.CleanFull.verified.txt index 924e49978..976ef7a1d 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.CleanFull.verified.txt +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.CleanFull.verified.txt @@ -42,18 +42,21 @@ { Id: Guid_1, DataType: mainLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, { Id: Guid_2, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, { Id: Guid_3, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.CleanIncremental.verified.txt b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.CleanIncremental.verified.txt index 22893f2e6..5e7d49305 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.CleanIncremental.verified.txt +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.CleanIncremental.verified.txt @@ -114,6 +114,7 @@ DataElement: { Id: Guid_4, DataType: mainLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, @@ -136,7 +137,8 @@ EnablePdfCreation: true, EnableFileScan: false, ValidationErrorOnPendingFileScan: false - } + }, + ContentType: application/xml }, { PreviousFormData: { @@ -162,6 +164,7 @@ DataElement: { Id: Guid_5, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, @@ -185,7 +188,8 @@ EnablePdfCreation: true, EnableFileScan: false, ValidationErrorOnPendingFileScan: false - } + }, + ContentType: application/xml }, { PreviousFormData: { @@ -221,6 +225,7 @@ DataElement: { Id: Guid_7, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, @@ -244,7 +249,8 @@ EnablePdfCreation: true, EnableFileScan: false, ValidationErrorOnPendingFileScan: false - } + }, + ContentType: application/xml } ] }, @@ -258,18 +264,21 @@ { Id: Guid_4, DataType: mainLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, { Id: Guid_5, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, { Id: Guid_7, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true } @@ -333,18 +342,21 @@ { Id: Guid_4, DataType: mainLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, { Id: Guid_5, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, { Id: Guid_7, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.DirtyFull.verified.txt b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.DirtyFull.verified.txt index e739edaa2..526127815 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.DirtyFull.verified.txt +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.DirtyFull.verified.txt @@ -42,18 +42,21 @@ { Id: Guid_1, DataType: mainLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, { Id: Guid_2, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, { Id: Guid_3, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.DirtyIncremental.verified.txt b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.DirtyIncremental.verified.txt index bd0c8da13..303201abe 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.DirtyIncremental.verified.txt +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/CleanDataAccessor/TestValidateCleanData.DirtyIncremental.verified.txt @@ -135,6 +135,7 @@ DataElement: { Id: Guid_6, DataType: mainLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, @@ -157,7 +158,8 @@ EnablePdfCreation: true, EnableFileScan: false, ValidationErrorOnPendingFileScan: false - } + }, + ContentType: application/xml }, { PreviousFormData: { @@ -184,6 +186,7 @@ DataElement: { Id: Guid_7, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, @@ -207,7 +210,8 @@ EnablePdfCreation: true, EnableFileScan: false, ValidationErrorOnPendingFileScan: false - } + }, + ContentType: application/xml }, { PreviousFormData: { @@ -244,6 +248,7 @@ DataElement: { Id: Guid_9, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, @@ -267,7 +272,8 @@ EnablePdfCreation: true, EnableFileScan: false, ValidationErrorOnPendingFileScan: false - } + }, + ContentType: application/xml } ] }, @@ -281,18 +287,21 @@ { Id: Guid_6, DataType: mainLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, { Id: Guid_7, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, { Id: Guid_9, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true } @@ -357,18 +366,21 @@ { Id: Guid_6, DataType: mainLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, { Id: Guid_7, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true }, { Id: Guid_9, DataType: subLayout_dataType, + ContentType: application/xml, Locked: false, IsRead: true } diff --git a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/DataAccessorFixture.cs b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/DataAccessorFixture.cs index c497b96d1..cbb6eb8dc 100644 --- a/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/DataAccessorFixture.cs +++ b/test/Altinn.App.Core.Tests/LayoutExpressions/FullTests/DataAccessorFixture.cs @@ -217,14 +217,17 @@ public void AddFormData(object data, int? maxCount = null) throw new ArgumentException($"Data type {fullName} not found in ApplicationMetadata"); } var dataGuid = Guid.NewGuid(); - var dataElement = new DataElement() { Id = dataGuid.ToString(), DataType = dataType.Id }; + var dataElement = new DataElement() + { + Id = dataGuid.ToString(), + DataType = dataType.Id, + ContentType = "application/xml", + }; Instance.Data.Add(dataElement); var serializationService = new ModelSerializationService(AppModelMock.Object); DataClientMock .Setup(dc => dc.GetDataBytes( - Org, - App, InstanceOwnerPartyId, InstanceGuid, dataGuid, @@ -232,6 +235,6 @@ public void AddFormData(object data, int? maxCount = null) It.IsAny() ) ) - .ReturnsAsync(serializationService.SerializeToStorage(data, dataType).data.ToArray()); + .ReturnsAsync(serializationService.SerializeToStorage(data, dataType, dataElement).data.ToArray()); } } 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..b44a39800 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs @@ -0,0 +1,275 @@ +using Altinn.App.Core.Helpers.Serialization; +using Altinn.App.Core.Internal.AppModel; +using Altinn.Platform.Storage.Interface.Models; +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, null); + + // Assert + Assert.Equal(expectedOutputType, outputContentType); + } + + [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, null); + + // Assert + var json = System.Text.Encoding.UTF8.GetString(data.Span); + + Assert.Equal("{\"name\":\"Test\",\"value\":42}", json); + } + + [Fact] + public void SerializeToStorage_SerializesXmlWithObsoleteMethod() + { + // 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); + + Assert.Equal( + "Test42", + xml + ); + } + + [Fact] + public void SerializeToStorage_SerializesXmlWithNullDataElement() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + + var dataType = CreateDataType(["application/xml"]); + + // Act + var (data, _) = _sut.SerializeToStorage(testObject, dataType, dataElement: null); + + // Assert + var xml = System.Text.Encoding.UTF8.GetString(data.Span); + + Assert.Equal( + "Test42", + xml + ); + } + + [Fact] + public void SerializeToStorage_SerializesJsonOnOldElement() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + + var dataType = CreateDataType(["application/xml"]); + + // Act + var (data, _) = _sut.SerializeToStorage( + testObject, + dataType, + new DataElement { ContentType = "application/json" } + ); + + // Assert + var json = System.Text.Encoding.UTF8.GetString(data.Span); + Assert.Equal("{\"name\":\"Test\",\"value\":42}", json); + } + + [Fact] + public void SerializeToStorage_DefaultsToXmlOnUnsupportedContentType() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + + var dataType = CreateDataType(["application/unsupported"]); + + // Act + var (data, contentType) = _sut.SerializeToStorage(testObject, dataType, null); + + // Assert + Assert.Equal("application/xml", contentType); + } + + [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, null); + }; + + // Assert + Assert.Throws(act); + } + + [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, null); + + // Assert + Assert.Equal("application/json", outputContentType); + } + + [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 + Assert.Throws(act); + } + + [Fact] + public void DeserializeFromStorage_DeserializesJson() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + var data = _sut.SerializeToJson(testObject); + + var dataType = CreateDataType(["application/json", "application/xml"]); + var dataElement = new DataElement() { ContentType = "application/json" }; + + // Act + var result = _sut.DeserializeFromStorage(data.Span, dataType, dataElement); + + // Assert + Assert.IsType(result); + var model = (TestDataModel)result; + Assert.Equal("Test", model.Name); + Assert.Equal(42, model.Value); + } + + [Fact] + public void DeserializeFromStorage_DeserializesXml() + { + // Arrange + var testObject = new TestDataModel { Name = "Test", Value = 42 }; + var data = _sut.SerializeToXml(testObject); + + var dataType = CreateDataType(["application/xml", "application/json"]); + var dataElement = new DataElement() { ContentType = "application/xml" }; + + // Act + var result = _sut.DeserializeFromStorage(data.Span, dataType, dataElement); + + // Assert + Assert.IsType(result); + var model = (TestDataModel)result; + Assert.Equal("Test", model.Name); + Assert.Equal(42, model.Value); + } + + [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 + Assert.IsType(result); + var model = (TestDataModel)result; + Assert.Equal("Test", model.Name); + Assert.Equal(42, model.Value); + } + + 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 7f18db8b7..b85f004fe 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 @@ -2257,16 +2257,23 @@ namespace Altinn.App.Core.Helpers.Serialization public sealed class ModelSerializationService { public ModelSerializationService(Altinn.App.Core.Internal.AppModel.IAppModel appModel, Altinn.App.Core.Features.Telemetry? telemetry = null) { } + [System.Obsolete("DeserializeFromStorage needs a DataElement parameter to support json in storage")] 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, Altinn.Platform.Storage.Interface.Models.DataElement dataElement) { } 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) { } public object GetEmpty(Altinn.Platform.Storage.Interface.Models.DataType dataType) { } public System.ReadOnlyMemory SerializeToJson(object model) { } + [System.Obsolete("SerializeToStorage needs a DataElement parameter to support json in storage")] [return: System.Runtime.CompilerServices.TupleElementNames(new string[] { "data", "contentType"})] public System.ValueTuple, string> SerializeToStorage(object model, Altinn.Platform.Storage.Interface.Models.DataType dataType) { } + [return: System.Runtime.CompilerServices.TupleElementNames(new string[] { + "data", + "contentType"})] + public System.ValueTuple, string> SerializeToStorage(object model, Altinn.Platform.Storage.Interface.Models.DataType dataType, Altinn.Platform.Storage.Interface.Models.DataElement? dataElement) { } public System.ReadOnlyMemory SerializeToXml(object model) { } } } @@ -2451,29 +2458,36 @@ namespace Altinn.App.Core.Infrastructure.Clients.Storage public ApplicationClient(Microsoft.Extensions.Options.IOptions platformSettings, Microsoft.Extensions.Logging.ILogger logger, System.Net.Http.HttpClient httpClient) { } public System.Threading.Tasks.Task GetApplication(string org, string app) { } } - public class DataClient : Altinn.App.Core.Internal.Data.IDataClient + public sealed class DataClient : Altinn.App.Core.Internal.Data.IDataClient { public DataClient(System.Net.Http.HttpClient httpClient, System.IServiceProvider serviceProvider) { } - public System.Threading.Tasks.Task DeleteBinaryData(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataGuid) { } - public System.Threading.Tasks.Task DeleteData(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataGuid, bool delay, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } - public System.Threading.Tasks.Task GetBinaryData(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } - public System.Threading.Tasks.Task> GetBinaryDataList(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } + public System.Threading.Tasks.Task DeleteData(int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataGuid, bool delay, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } + public System.Threading.Tasks.Task GetBinaryData(int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } + public System.Threading.Tasks.Task> GetBinaryDataList(int instanceOwnerPartyId, System.Guid instanceGuid, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.Tasks.Task GetBinaryDataStream(int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.TimeSpan? timeout = default, System.Threading.CancellationToken cancellationToken = default) { } - public System.Threading.Tasks.Task GetDataBytes(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } + public System.Threading.Tasks.Task GetDataBytes(int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } + public System.Threading.Tasks.Task GetFormData(Altinn.Platform.Storage.Interface.Models.Instance instance, Altinn.Platform.Storage.Interface.Models.DataElement dataElement, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } + [System.Obsolete("Use the overload with Instance parameter instead")] public System.Threading.Tasks.Task GetFormData(System.Guid instanceGuid, System.Type type, string org, string app, int instanceOwnerPartyId, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.Tasks.Task InsertBinaryData(string instanceId, string dataType, string contentType, string? filename, System.IO.Stream stream, string? generatedFromTask = null, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } + [System.Obsolete("The overload that takes a HttpRequest is deprecated, use the overload that takes " + + "a Stream instead")] public System.Threading.Tasks.Task InsertBinaryData(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, string dataType, Microsoft.AspNetCore.Http.HttpRequest request, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } - public System.Threading.Tasks.Task InsertFormData(Altinn.Platform.Storage.Interface.Models.Instance instance, string dataTypeString, T dataToSerialize, System.Type type, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) - where T : notnull { } + public System.Threading.Tasks.Task InsertFormData(Altinn.Platform.Storage.Interface.Models.Instance instance, string dataTypeId, object dataToSerialize, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } + [System.Obsolete("Use InsertFormData with Instance parameter instead")] public System.Threading.Tasks.Task InsertFormData(T dataToSerialize, System.Guid instanceGuid, System.Type type, string org, string app, int instanceOwnerPartyId, string dataType, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) where T : notnull { } public System.Threading.Tasks.Task LockDataElement(Altinn.App.Core.Models.InstanceIdentifier instanceIdentifier, System.Guid dataGuid, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.Tasks.Task UnlockDataElement(Altinn.App.Core.Models.InstanceIdentifier instanceIdentifier, System.Guid dataGuid, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.Tasks.Task Update(Altinn.Platform.Storage.Interface.Models.Instance instance, Altinn.Platform.Storage.Interface.Models.DataElement dataElement, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.Tasks.Task UpdateBinaryData(Altinn.App.Core.Models.InstanceIdentifier instanceIdentifier, string? contentType, string? filename, System.Guid dataGuid, System.IO.Stream stream, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } + [System.Obsolete("The overload that takes a HttpRequest is deprecated, use the overload that takes " + + "a Stream instead")] public System.Threading.Tasks.Task UpdateBinaryData(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataGuid, Microsoft.AspNetCore.Http.HttpRequest request, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } + [System.Obsolete("Use the UpdateFormData method with Instance parameter instead")] public System.Threading.Tasks.Task UpdateData(T dataToSerialize, System.Guid instanceGuid, System.Type type, string org, string app, int instanceOwnerPartyId, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) where T : notnull { } + public System.Threading.Tasks.Task UpdateFormData(Altinn.Platform.Storage.Interface.Models.Instance instance, object dataToSerialize, Altinn.Platform.Storage.Interface.Models.DataElement dataElement, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } } public class InstanceClient : Altinn.App.Core.Internal.Instances.IInstanceClient { @@ -2937,16 +2951,31 @@ namespace Altinn.App.Core.Internal.Data { [System.Obsolete("Use method DeleteData with delayed=false instead.", true)] System.Threading.Tasks.Task DeleteBinaryData(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataGuid); + System.Threading.Tasks.Task DeleteData(int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataGuid, bool delay, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + [System.Obsolete("Use the overload without org and app parameters")] System.Threading.Tasks.Task DeleteData(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataGuid, bool delay, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task GetBinaryData(int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + [System.Obsolete("Org and App parameters are not used, use the overload without these parameters")] System.Threading.Tasks.Task GetBinaryData(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task> GetBinaryDataList(int instanceOwnerPartyId, System.Guid instanceGuid, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + [System.Obsolete("Org and App parameters are not used, use the overload without these parameters")] System.Threading.Tasks.Task> GetBinaryDataList(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task GetBinaryDataStream(int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.TimeSpan? timeout = default, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task GetDataBytes(int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + [System.Obsolete("Org and App parameters are not used, use the overload without these parameters")] System.Threading.Tasks.Task GetDataBytes(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task GetFormData(Altinn.Platform.Storage.Interface.Models.Instance instance, Altinn.Platform.Storage.Interface.Models.DataElement dataElement, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + [System.Obsolete("Use the overload with Instance parameter instead")] System.Threading.Tasks.Task GetFormData(System.Guid instanceGuid, System.Type type, string org, string app, int instanceOwnerPartyId, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task InsertBinaryData(string instanceId, string dataType, string contentType, string? filename, System.IO.Stream stream, string? generatedFromTask = null, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + [System.Obsolete("The overload that takes a HttpRequest is deprecated, use the overload that takes " + + "a Stream instead")] System.Threading.Tasks.Task InsertBinaryData(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, string dataType, Microsoft.AspNetCore.Http.HttpRequest request, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task InsertFormData(Altinn.Platform.Storage.Interface.Models.Instance instance, string dataTypeId, object dataToSerialize, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + [System.Obsolete("Use the overload without Type parameter")] System.Threading.Tasks.Task InsertFormData(Altinn.Platform.Storage.Interface.Models.Instance instance, string dataTypeString, T dataToSerialize, System.Type type, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) where T : notnull; + [System.Obsolete("Use InsertFormData with Instance parameter instead")] System.Threading.Tasks.Task InsertFormData(T dataToSerialize, System.Guid instanceGuid, System.Type type, string org, string app, int instanceOwnerPartyId, string dataType, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) where T : notnull; System.Threading.Tasks.Task LockDataElement(Altinn.App.Core.Models.InstanceIdentifier instanceIdentifier, System.Guid dataGuid, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); @@ -2956,8 +2985,14 @@ namespace Altinn.App.Core.Internal.Data [System.Obsolete("Deprecated please use UpdateBinaryData(InstanceIdentifier, string, string, Guid, " + "Stream) instead", false)] System.Threading.Tasks.Task UpdateBinaryData(string org, string app, int instanceOwnerPartyId, System.Guid instanceGuid, System.Guid dataGuid, Microsoft.AspNetCore.Http.HttpRequest request, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + [System.Obsolete("Use the UpdateFormData method with Instance parameter instead")] System.Threading.Tasks.Task UpdateData(T dataToSerialize, System.Guid instanceGuid, System.Type type, string org, string app, int instanceOwnerPartyId, System.Guid dataId, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) where T : notnull; + System.Threading.Tasks.Task UpdateFormData(Altinn.Platform.Storage.Interface.Models.Instance instance, object dataToSerialize, Altinn.Platform.Storage.Interface.Models.DataElement dataElement, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default); + } + public static class IDataClientExtensions + { + public static System.Threading.Tasks.Task GetFormData(this Altinn.App.Core.Internal.Data.IDataClient dataClient, Altinn.Platform.Storage.Interface.Models.Instance instance, Altinn.Platform.Storage.Interface.Models.DataElement dataElement, Altinn.App.Core.Features.StorageAuthenticationMethod? authenticationMethod = null, System.Threading.CancellationToken cancellationToken = default) { } } public interface IDataService { diff --git a/test/Altinn.App.Tests.Common/Fixtures/MockedServiceCollection.cs b/test/Altinn.App.Tests.Common/Fixtures/MockedServiceCollection.cs index 0bc8501a6..9d9fd6d50 100644 --- a/test/Altinn.App.Tests.Common/Fixtures/MockedServiceCollection.cs +++ b/test/Altinn.App.Tests.Common/Fixtures/MockedServiceCollection.cs @@ -1,16 +1,14 @@ using System.Collections.Immutable; -using System.Text; using System.Text.Json; -using System.Xml; -using System.Xml.Serialization; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; using Altinn.App.Core.Helpers.Serialization; +using Altinn.App.Core.Infrastructure.Clients.Storage; 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.Language; +using Altinn.App.Core.Internal.Instances; using Altinn.App.Core.Internal.Texts; using Altinn.App.Core.Internal.Validation; using Altinn.App.Core.Models; @@ -39,14 +37,9 @@ public class MockedServiceCollection public AppSettings AppSettings { get; } = new AppSettings(); public GeneralSettings GeneralSettings { get; } = new GeneralSettings(); - public ApplicationMetadata AppMetadata { get; } = - new($"{Org}/{App}") - { - Title = new() { [LanguageConst.Nb] = "Testapplikasjon", [LanguageConst.En] = "Test Application" }, - DataTypes = [], - }; + public ApplicationMetadata AppMetadata => Storage.AppMetadata; - public StorageClientInterceptor Storage { get; } = new(); + public StorageClientInterceptor Storage { get; } public Mock AppResourcesMock { get; } = new(MockBehavior.Strict); public Mock AppMetadataMock { get; } = new(MockBehavior.Strict); @@ -60,6 +53,7 @@ public class MockedServiceCollection public MockedServiceCollection() { + Storage = new StorageClientInterceptor(); _services.AddSingleton(this); } @@ -70,42 +64,43 @@ public void AddXunitLogging(ITestOutputHelper outputHelper) public void TryAddCommonServices() { - AppImplementationFactoryExtensions.AddAppImplementationFactory(_services); + _services.AddAppImplementationFactory(); // Adding options - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services, Options.Create(PlatformSettings)); - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services, Options.Create(AppSettings)); - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services, Options.Create(GeneralSettings)); + _services.TryAddSingleton(Options.Create(PlatformSettings)); + _services.TryAddSingleton(Options.Create(AppSettings)); + _services.TryAddSingleton(Options.Create(GeneralSettings)); - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services, new AppIdentifier(Org, App)); + _services.TryAddSingleton(new AppIdentifier(Org, App)); // Adding Validation infrastructure - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services); - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services); + _services.TryAddSingleton(); + _services.TryAddSingleton(); // Adding Translation infrastructure - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services); + _services.TryAddSingleton(); // InstanceDataUnitOfWork - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services); - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services); + _services.TryAddSingleton(); + _services.TryAddSingleton(); - // Just add the httpClients without a branch - Storage.AddStorageClients(_services); + // There is no TryAddHttpClient, but these are the core of the mocked service collection + _services.AddHttpClient().ConfigurePrimaryHttpMessageHandler(() => Storage); + _services.AddHttpClient().ConfigurePrimaryHttpMessageHandler(() => Storage); // Ensure logging is present - var hasLogger = Enumerable.Any(_services, s => s.ServiceType == typeof(ILoggerFactory)); + var hasLogger = _services.Any(s => s.ServiceType == typeof(ILoggerFactory)); if (!hasLogger) { - ServiceCollectionServiceExtensions.AddSingleton(_services, NullLoggerFactory.Instance); + _services.AddSingleton(NullLoggerFactory.Instance); } // Add standard mocks - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services, AppResourcesMock.Object); - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services, AppMetadataMock.Object); - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services, AppModelMock.Object); - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services, AuthenticationTokenResolverMock.Object); - ServiceCollectionDescriptorExtensions.TryAddSingleton(_services, UserTokenProviderMock.Object); + _services.TryAddSingleton(AppResourcesMock.Object); + _services.TryAddSingleton(AppMetadataMock.Object); + _services.TryAddSingleton(AppModelMock.Object); + _services.TryAddSingleton(AuthenticationTokenResolverMock.Object); + _services.TryAddSingleton(UserTokenProviderMock.Object); // Setup default mock behaviours AppMetadataMock.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(AppMetadata); @@ -126,22 +121,6 @@ public void TryAddCommonServices() ); } - private static byte[] SerializeXml(T model) - where T : class, new() - { - XmlWriterSettings xmlWriterSettings = new XmlWriterSettings() - { - Encoding = new UTF8Encoding(false), - NewLineHandling = NewLineHandling.None, - }; - using var memoryStream = new MemoryStream(); - using XmlWriter xmlWriter = XmlWriter.Create(memoryStream, xmlWriterSettings); - - XmlSerializer serializer = new XmlSerializer(model.GetType()); - serializer.Serialize(xmlWriter, model); - return memoryStream.ToArray(); - } - public void AddDataType(DataType dataType) { lock (AppMetadata.DataTypes) @@ -150,20 +129,25 @@ public void AddDataType(DataType dataType) } } - public void AddDataType(DataType dataType) + public DataType AddDataType(string? dataTypeId = null, string[]? allowedContentTypes = null, int maxCount = 1) where T : class, new() { var classRef = typeof(T).FullName ?? throw new InvalidOperationException("DataType for formData does not have a ClassRef defined."); - - dataType.AppLogic ??= new(); - dataType.AppLogic.ClassRef = classRef; + var dataType = new DataType() + { + Id = dataTypeId ?? typeof(T).Name.ToLowerInvariant(), + AppLogic = new() { ClassRef = classRef }, + MaxCount = maxCount, + AllowedContentTypes = allowedContentTypes?.ToList() ?? ["application/xml"], + }; AppModelMock.Setup(a => a.GetModelType(classRef)).Returns(typeof(T)); - AppModelMock.Setup(a => a.Create(classRef)).Returns(new T()); + AppModelMock.Setup(a => a.Create(classRef)).Returns(() => new T()); AddDataType(dataType); + return dataType; } private readonly Dictionary _textResources = new(); @@ -188,6 +172,14 @@ public void AddTextResource(string language, TextResourceElement textResource) public ServiceProvider BuildServiceProvider() => ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(_services); + + public void VerifyMocks() + { + AppMetadataMock.Verify(); + AppModelMock.Verify(); + AuthenticationTokenResolverMock.Verify(); + AppResourcesMock.Verify(); + } } public static class MockedServiceProviderExtensions @@ -201,6 +193,7 @@ internal static async Task CreateInstanceDataMutatorWith where T : class, new() { var appServices = serviceProvider.GetRequiredService(); + var serializer = serviceProvider.GetRequiredService(); var instanceGuid = Guid.NewGuid(); var dataGuid = Guid.NewGuid(); var partyId = 123456; @@ -224,14 +217,7 @@ internal static async Task CreateInstanceDataMutatorWith TextResourceBindings = ImmutableDictionary.Empty, }); - DataType defaultDataType = new() - { - Id = dataTypeId, - MaxCount = 1, - AllowedContentTypes = ["application/xml"], - }; - - appServices.AddDataType(defaultDataType); + DataType defaultDataType = appServices.AddDataType(); var layoutModel = new LayoutModel( [new LayoutSetComponent(pages.ToList(), layoutSetName, defaultDataType)], @@ -249,6 +235,7 @@ [new LayoutSetComponent(pages.ToList(), layoutSetName, defaultDataType)], Id = dataGuid.ToString(), InstanceGuid = instanceGuid.ToString(), DataType = defaultDataType.Id, + ContentType = "application/xml", }; var instance = new Instance() { @@ -260,27 +247,14 @@ [new LayoutSetComponent(pages.ToList(), layoutSetName, defaultDataType)], }; appServices.Storage.AddInstance(instance); - appServices.Storage.AddData(dataGuid, SerializeXml(model)); + appServices.Storage.AddDataRaw( + dataGuid, + serializer.SerializeToStorage(model, defaultDataType, data).data.ToArray() + ); var instanceCopy = JsonSerializer.Deserialize(JsonSerializer.SerializeToUtf8Bytes(instance))!; var initializer = serviceProvider.GetRequiredService(); return await initializer.Init(instanceCopy, taskId, language); } - - private static byte[] SerializeXml(T model) - where T : class, new() - { - XmlWriterSettings xmlWriterSettings = new XmlWriterSettings() - { - Encoding = new UTF8Encoding(false), - NewLineHandling = NewLineHandling.None, - }; - using var memoryStream = new MemoryStream(); - using XmlWriter xmlWriter = XmlWriter.Create(memoryStream, xmlWriterSettings); - - XmlSerializer serializer = new XmlSerializer(model.GetType()); - serializer.Serialize(xmlWriter, model); - return memoryStream.ToArray(); - } } diff --git a/test/Altinn.App.Tests.Common/Fixtures/StorageClientInterceptor.cs b/test/Altinn.App.Tests.Common/Fixtures/StorageClientInterceptor.cs index 7fdd8e66b..e4ca8d250 100644 --- a/test/Altinn.App.Tests.Common/Fixtures/StorageClientInterceptor.cs +++ b/test/Altinn.App.Tests.Common/Fixtures/StorageClientInterceptor.cs @@ -1,32 +1,73 @@ using System.Collections.Concurrent; +using System.Net; using System.Net.Http.Headers; -using Altinn.App.Core.Infrastructure.Clients.Storage; -using Altinn.App.Core.Internal.Data; -using Altinn.App.Core.Internal.Instances; +using System.Text; +using System.Web; +using Altinn.App.Core.Internal.Language; +using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; -using Microsoft.Extensions.DependencyInjection; namespace Altinn.App.Tests.Common.Mocks; public class StorageClientInterceptor : HttpMessageHandler { - public void AddStorageClients(IServiceCollection services) + public class RequestResponse(HttpRequestMessage Request, byte[]? requestBody, HttpResponseMessage Response) { - services.AddHttpClient().ConfigurePrimaryHttpMessageHandler(() => this); - services.AddHttpClient().ConfigurePrimaryHttpMessageHandler(() => this); - } + public Uri? RequestUrl { get; } = Request.RequestUri; + public HttpMethod RequestMethod { get; } = Request.Method; + public string? RequestBody { get; } = requestBody is null ? null : Encoding.UTF8.GetString(requestBody); + public HttpRequestHeaders RequestHeaders { get; } = Request.Headers; + public HttpContentHeaders? RequestContentHeaders { get; } = Request.Content?.Headers; + + public HttpStatusCode ResponseStatusCode { get; } = Response.StatusCode; + public string ResponseBody { get; } = Response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + public HttpResponseHeaders ResponseHeaders { get; } = Response.Headers; + public HttpContentHeaders ResponseContentHeaders { get; } = Response.Content.Headers; + }; private ConcurrentDictionary _instances = new(); private ConcurrentDictionary _data = new(); + public StorageClientInterceptor(string org = "ttd", string app = "mocked-test-app") + { + AppMetadata = new($"{org}/{app}") + { + Title = new Dictionary + { + { LanguageConst.Nb, "Testapplikasjon" }, + { LanguageConst.En, "Mocked Test App" }, + }, + DataTypes = [], + }; + } + + public ConcurrentBag RequestsResponses { get; } = new(); + public ApplicationMetadata AppMetadata { get; } + public void AddInstance(Instance instance) { + instance.Data ??= []; _instances[instance.Id] = instance; } - public void AddData(Guid dataId, byte[] data) + public void AddDataRaw(Guid dataId, byte[] data) + { + _data[dataId] = data; + } + + public DataElement AddData(Instance instance, string dataType, string contentType, byte[] data) { + var dataId = Guid.NewGuid(); + var dataElement = new DataElement() + { + Id = dataId.ToString(), + DataType = dataType, + ContentType = contentType, + Size = data.Length, + }; + instance.Data.Add(dataElement); _data[dataId] = data; + return dataElement; } protected override async Task SendAsync( @@ -38,7 +79,7 @@ CancellationToken cancellationToken ? await request.Content.ReadAsByteArrayAsync(cancellationToken) : null; - return (request.Method.Method, request.RequestUri?.AbsolutePath.Trim('/').Split('/')) switch + var response = (request.Method.Method, request.RequestUri?.AbsolutePath.Trim('/').Split('/')) switch { ("GET", { } path) when TryParseInstanceUrl(path, out int partyId, out Guid instanceGuid) => GetInstance( partyId, @@ -46,17 +87,139 @@ CancellationToken cancellationToken ), ("GET", { } path) when TryParseDataUrl(path, out int partyId, out Guid instanceGuid, out Guid dataId) => GetData(partyId, instanceGuid, dataId), + ("POST", { } path) when TryParseDataPostUrl(path, out int partyId, out Guid instanceGuid) => PostData( + partyId, + instanceGuid, + requestBody, + request + ), + ("PUT", { } path) when TryParseDataUrl(path, out int partyId, out Guid instanceGuid, out Guid dataId) => + PutData(partyId, instanceGuid, dataId, requestBody, request), var (method, _) => throw new Exception( $"Unhandled request to {request.RequestUri?.AbsolutePath} with method {method} and body\n\n{(requestBody is not null ? System.Text.Encoding.UTF8.GetString(requestBody) : "[no body]")}" ), }; + RequestsResponses.Add(new RequestResponse(request, requestBody, response)); + return response; + } + + private HttpResponseMessage PutData( + int instanceOwnerPartyId, + Guid instanceGuid, + Guid dataId, + byte[]? dataContent, + HttpRequestMessage request + ) + { + if (dataContent is null) + { + return CreateErrorResponse(HttpStatusCode.BadRequest, "No data content provided in PutData request"); + } + + if (!_instances.TryGetValue($"{instanceOwnerPartyId}/{instanceGuid}", out var instance)) + { + return CreateErrorResponse( + HttpStatusCode.NotFound, + $"Instance with id {instanceOwnerPartyId}/{instanceGuid} not found" + ); + } + var dataElement = instance.Data.FirstOrDefault(de => de.Id == dataId.ToString()); + if (dataElement == null) + { + return CreateErrorResponse( + HttpStatusCode.NotFound, + $"Data element with id {dataId} not found in instance {instanceOwnerPartyId}/{instanceGuid}" + ); + } + + var dataType = AppMetadata.DataTypes.Find(dt => dt.Id == dataElement.DataType); + if (dataType == null) + { + return CreateErrorResponse( + HttpStatusCode.BadRequest, + $"Data type \"{dataElement.DataType ?? "null"}\" not found in application metadata" + ); + } + + var contentType = request.Content?.Headers.ContentType?.MediaType; + if (contentType != dataElement.ContentType) + { + return CreateErrorResponse( + HttpStatusCode.BadRequest, + $"Content type {contentType} does not match existing content type {dataElement.ContentType} for data element {dataId}" + ); + } + dataElement.Size = dataContent.Length; + _data[dataId] = dataContent; + + var dataElementJson = System.Text.Json.JsonSerializer.Serialize(dataElement); + return new HttpResponseMessage(HttpStatusCode.Created) + { + Content = new StringContent(dataElementJson, Encoding.UTF8, "application/json"), + }; + } + + private bool TryParseInstanceUrl(string[] pathSegments, out int instanceOwnerPartyId, out Guid instanceGuid) + { + if ( + pathSegments is ["storage", "api", "v1", "instances", var partyIdStr, var instanceGuidStr] + && int.TryParse(partyIdStr, out instanceOwnerPartyId) + && Guid.TryParse(instanceGuidStr, out instanceGuid) + ) + { + return true; + } + + instanceOwnerPartyId = 0; + instanceGuid = Guid.Empty; + return false; + } + + private HttpResponseMessage GetInstance(int partyId, Guid instanceGuid) + { + if (!_instances.TryGetValue($"{partyId}/{instanceGuid}", out Instance? instance)) + { + return new HttpResponseMessage(HttpStatusCode.NotFound) + { + Content = new StringContent($"Instance with id {instanceGuid} not found"), + }; + } + var instanceJson = System.Text.Json.JsonSerializer.Serialize(instance); + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(instanceJson, System.Text.Encoding.UTF8, "application/json"), + }; + } + + private bool TryParseDataUrl( + string[] pathSegments, + out int instanceOwnerPartyId, + out Guid instanceGuid, + out Guid dataId + ) + { + if ( + pathSegments + is ["storage", "api", "v1", "instances", var partyIdStr, var instanceGuidStr, "data", var dataIdStr] + && int.TryParse(partyIdStr, out instanceOwnerPartyId) + && Guid.TryParse(instanceGuidStr, out instanceGuid) + && Guid.TryParse(dataIdStr, out dataId) + ) + { + return true; + } + + instanceOwnerPartyId = 0; + instanceGuid = Guid.Empty; + dataId = Guid.Empty; + return false; } private HttpResponseMessage GetData(int partyId, Guid instanceGuid, Guid dataId) { if (!_instances.TryGetValue($"{partyId}/{instanceGuid}", out Instance? instance)) { - return new HttpResponseMessage(System.Net.HttpStatusCode.NotFound) + return new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent($"Instance with id {instanceGuid} not found"), }; @@ -64,7 +227,7 @@ private HttpResponseMessage GetData(int partyId, Guid instanceGuid, Guid dataId) var dataElement = instance.Data.FirstOrDefault(de => de.Id == dataId.ToString()); if (dataElement == null) { - return new HttpResponseMessage(System.Net.HttpStatusCode.NotFound) + return new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent($"Data element with id {dataId} not found in instance {instanceGuid}"), }; @@ -72,12 +235,12 @@ private HttpResponseMessage GetData(int partyId, Guid instanceGuid, Guid dataId) if (!_data.TryGetValue(dataId, out var storedData)) { - return new HttpResponseMessage(System.Net.HttpStatusCode.NotFound) + return new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent($"Data with id {dataId} not found"), }; } - return new HttpResponseMessage(System.Net.HttpStatusCode.OK) + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(storedData) { @@ -89,15 +252,10 @@ private HttpResponseMessage GetData(int partyId, Guid instanceGuid, Guid dataId) }; } - private HttpResponseMessage GetInstance(int partyId, Guid instanceGuid) - { - throw new NotImplementedException(); - } - - public bool TryParseInstanceUrl(string[] pathSegments, out int instanceOwnerPartyId, out Guid instanceGuid) + private bool TryParseDataPostUrl(string[] pathSegments, out int instanceOwnerPartyId, out Guid instanceGuid) { if ( - pathSegments is ["storage", "api", "v1", "instances", var partyIdStr, var instanceGuidStr] + pathSegments is ["storage", "api", "v1", "instances", var partyIdStr, var instanceGuidStr, "data"] && int.TryParse(partyIdStr, out instanceOwnerPartyId) && Guid.TryParse(instanceGuidStr, out instanceGuid) ) @@ -110,27 +268,66 @@ public bool TryParseInstanceUrl(string[] pathSegments, out int instanceOwnerPart return false; } - public bool TryParseDataUrl( - string[] pathSegments, - out int instanceOwnerPartyId, - out Guid instanceGuid, - out Guid dataId + private HttpResponseMessage PostData( + int instanceOwnerPartyId, + Guid instanceGuid, + byte[]? dataContent, + HttpRequestMessage request ) { - if ( - pathSegments - is ["storage", "api", "v1", "instances", var partyIdStr, var instanceGuidStr, "data", var dataIdStr] - && int.TryParse(partyIdStr, out instanceOwnerPartyId) - && Guid.TryParse(instanceGuidStr, out instanceGuid) - && Guid.TryParse(dataIdStr, out dataId) - ) + if (dataContent is null) { - return true; + return CreateErrorResponse(HttpStatusCode.BadRequest, "No data content provided in PostData request"); } + var queryParam = HttpUtility.ParseQueryString( + request.RequestUri?.Query ?? throw new Exception("Request URI is null") + ); + var dataTypeString = + queryParam["dataType"] ?? throw new Exception("Data type is not specified in PostData request"); - instanceOwnerPartyId = 0; - instanceGuid = Guid.Empty; - dataId = Guid.Empty; - return false; + var dataType = AppMetadata.DataTypes.FirstOrDefault(dt => dt.Id == dataTypeString); + if (dataType == null) + { + return CreateErrorResponse( + HttpStatusCode.BadRequest, + $"Data type {dataTypeString} not found in application metadata" + ); + } + + var contentType = request.Content?.Headers.ContentType?.MediaType; + if (contentType is null || !dataType.AllowedContentTypes.Contains(contentType)) + { + return CreateErrorResponse( + HttpStatusCode.BadRequest, + $"Data type {dataTypeString} does not allow content type {contentType}, allowed types are: {string.Join(", ", dataType.AllowedContentTypes)}" + ); + } + + if (!_instances.TryGetValue($"{instanceOwnerPartyId}/{instanceGuid}", out Instance? instance)) + { + return CreateErrorResponse( + HttpStatusCode.NotFound, + $"Instance with id {instanceOwnerPartyId}/{instanceGuid} not found" + ); + } + + var dataElement = AddData(instance, dataTypeString, contentType, dataContent); + var dataElementJson = System.Text.Json.JsonSerializer.Serialize(dataElement); + return new HttpResponseMessage(HttpStatusCode.Created) + { + Content = new StringContent(dataElementJson, Encoding.UTF8, "application/json"), + }; + } + + private static HttpResponseMessage CreateErrorResponse( + HttpStatusCode status, + string stringContent, + string contentType = "text/plain" + ) + { + return new HttpResponseMessage(status) + { + Content = new StringContent(stringContent, Encoding.UTF8, contentType), + }; } }