From 2cecad6030b0bd082bc739492c30811293848224 Mon Sep 17 00:00:00 2001 From: Vemund Gaukstad Date: Mon, 24 Apr 2023 10:31:29 +0200 Subject: [PATCH] Introduce new method in IData for updating BinaryData (#228) (#231) * Introduce new method in IData for updating BinaryData without a HttpRequest as input. Old method marked as obsolete * Consolidate test classes and replace JwtTokenUtil with UserTokenProvider * Fix build errors * Add some more tests and fix nullability warnings --- .../Controllers/DataController.cs | 19 +- .../Clients/Storage/DataClient.cs | 59 +- .../Clients/Storage/TestData/ExampleModel.cs | 16 + src/Altinn.App.Core/Interface/IData.cs | 13 +- .../Implementation/DataClientTest.cs | 183 ------ .../Infrastructure/Clients/DataClientTests.cs | 530 ++++++++++++++++++ 6 files changed, 612 insertions(+), 208 deletions(-) create mode 100644 src/Altinn.App.Core/Infrastructure/Clients/Storage/TestData/ExampleModel.cs delete mode 100644 test/Altinn.App.Core.Tests/Implementation/DataClientTest.cs create mode 100644 test/Altinn.App.Core.Tests/Infrastructure/Clients/DataClientTests.cs diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index 1cc1f3374..dc1aebf4c 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -14,6 +14,8 @@ using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; namespace Altinn.App.Api.Controllers { @@ -263,7 +265,7 @@ public async Task Put( return errorResponse; } - return await PutBinaryData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid); + return await PutBinaryData(instanceOwnerPartyId, instanceGuid, dataGuid); } catch (PlatformHttpException e) { @@ -513,12 +515,19 @@ private async Task GetFormData( return Ok(appModel); } - private async Task PutBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid) + private async Task PutBinaryData(int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid) { - DataElement dataElement = await _dataClient.UpdateBinaryData(org, app, instanceOwnerPartyId, instanceGuid, dataGuid, Request); - SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request); + if (Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues)) + { + var contentDispositionHeader = ContentDispositionHeaderValue.Parse(headerValues.ToString()); + _logger.LogInformation("Content-Disposition: {ContentDisposition}", headerValues); + DataElement dataElement = await _dataClient.UpdateBinaryData(new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), Request.ContentType, contentDispositionHeader.FileName.ToString(), dataGuid, Request.Body); + SelfLinkHelper.SetDataAppSelfLinks(instanceOwnerPartyId, instanceGuid, dataElement, Request); - return Created(dataElement.SelfLinks.Apps, dataElement); + return Created(dataElement.SelfLinks.Apps, dataElement); + } + + return BadRequest("Invalid data provided. Error: The request must include a Content-Disposition header"); } private async Task PutFormData(string org, string app, Instance instance, Guid dataGuid, string dataType) diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index c1e31d97f..a5eef17d2 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -21,6 +21,7 @@ using Newtonsoft.Json; using System.Xml; +using Microsoft.IdentityModel.Tokens; namespace Altinn.App.Core.Infrastructure.Clients.Storage { @@ -31,8 +32,7 @@ public class DataClient : IData { private readonly PlatformSettings _platformSettings; private readonly ILogger _logger; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly AppSettings _settings; + private readonly IUserTokenProvider _userTokenProvider; private readonly HttpClient _client; /// @@ -40,26 +40,23 @@ public class DataClient : IData /// /// the platform settings /// the logger - /// The http context accessor - /// The current app settings. /// A HttpClient from the built in HttpClient factory. + /// Service to obtain json web token public DataClient( IOptions platformSettings, ILogger logger, - IHttpContextAccessor httpContextAccessor, - IOptionsMonitor settings, - HttpClient httpClient) + HttpClient httpClient, + IUserTokenProvider userTokenProvider) { _platformSettings = platformSettings.Value; _logger = logger; - _httpContextAccessor = httpContextAccessor; - _settings = settings.CurrentValue; httpClient.BaseAddress = new Uri(_platformSettings.ApiStorageEndpoint); httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); _client = httpClient; + _userTokenProvider = userTokenProvider; } /// @@ -77,7 +74,7 @@ public async Task InsertFormData(T dataToSerialize, Guid instanc public async Task InsertFormData(Instance instance, string dataType, T dataToSerialize, Type type) { string apiUrl = $"instances/{instance.Id}/data?dataType={dataType}"; - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + string token = _userTokenProvider.GetUserToken(); DataElement dataElement; using MemoryStream stream = new MemoryStream(); @@ -105,7 +102,7 @@ public async Task UpdateData(T dataToSerialize, Guid instanceGui { string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + string token = _userTokenProvider.GetUserToken(); using MemoryStream stream = new MemoryStream(); @@ -147,7 +144,7 @@ public async Task GetBinaryData(string org, string app, int instanceOwne string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + string token = _userTokenProvider.GetUserToken(); HttpResponseMessage response = await _client.GetAsync(token, apiUrl); @@ -168,7 +165,7 @@ public async Task GetFormData(Guid instanceGuid, Type type, string org, { string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"instances/{instanceIdentifier}/data/{dataId}"; - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + string token = _userTokenProvider.GetUserToken(); HttpResponseMessage response = await _client.GetAsync(token, apiUrl); if (response.IsSuccessStatusCode) @@ -194,7 +191,7 @@ public async Task> GetBinaryDataList(string org, string app { string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"instances/{instanceIdentifier}/dataelements"; - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + string token = _userTokenProvider.GetUserToken(); DataElementList dataList; List attachmentList = new List(); @@ -259,7 +256,7 @@ public async Task DeleteData(string org, string app, int instanceOwnerPart { string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"instances/{instanceIdentifier}/data/{dataGuid}?delay={delay}"; - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + string token = _userTokenProvider.GetUserToken(); HttpResponseMessage response = await _client.DeleteAsync(token, apiUrl); @@ -277,7 +274,7 @@ public async Task InsertBinaryData(string org, string app, int inst { string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data?dataType={dataType}"; - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + string token = _userTokenProvider.GetUserToken(); DataElement dataElement; StreamContent content = CreateContentStream(request); @@ -300,7 +297,7 @@ public async Task InsertBinaryData(string org, string app, int inst public async Task InsertBinaryData(string instanceId, string dataType, string contentType, string filename, Stream stream) { string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceId}/data?dataType={dataType}"; - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + string token = _userTokenProvider.GetUserToken(); DataElement dataElement; StreamContent content = new StreamContent(stream); @@ -333,7 +330,7 @@ public async Task UpdateBinaryData(string org, string app, int inst { string instanceIdentifier = $"{instanceOwnerPartyId}/{instanceGuid}"; string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}"; - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + string token = _userTokenProvider.GetUserToken(); StreamContent content = CreateContentStream(request); @@ -350,6 +347,30 @@ public async Task UpdateBinaryData(string org, string app, int inst _logger.LogError($"Updating attachment {dataGuid} for instance {instanceGuid} failed with status code {response.StatusCode}"); throw await PlatformHttpException.CreateAsync(response); } + + /// + public async Task UpdateBinaryData(InstanceIdentifier instanceIdentifier, string? contentType, string filename, Guid dataGuid, Stream stream) + { + string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}"; + string token = _userTokenProvider.GetUserToken(); + StreamContent content = new StreamContent(stream); + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + content.Headers.ContentDisposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment) + { + FileName = filename, + FileNameStar = filename + }; + HttpResponseMessage response = await _client.PutAsync(token, apiUrl, content); + _logger.LogInformation("Update binary data result: {ResultCode}", response.StatusCode); + if (response.IsSuccessStatusCode) + { + string instancedata = await response.Content.ReadAsStringAsync(); + DataElement dataElement = JsonConvert.DeserializeObject(instancedata)!; + + return dataElement; + } + throw await PlatformHttpException.CreateAsync(response); + } private static StreamContent CreateContentStream(HttpRequest request) { @@ -368,7 +389,7 @@ private static StreamContent CreateContentStream(HttpRequest request) public async Task Update(Instance instance, DataElement dataElement) { string apiUrl = $"{_platformSettings.ApiStorageEndpoint}instances/{instance.Id}/dataelements/{dataElement.Id}"; - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + string token = _userTokenProvider.GetUserToken(); StringContent jsonString = new StringContent(JsonConvert.SerializeObject(dataElement), Encoding.UTF8, "application/json"); HttpResponseMessage response = await _client.PutAsync(token, apiUrl, jsonString); diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/TestData/ExampleModel.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/TestData/ExampleModel.cs new file mode 100644 index 000000000..7643dbf51 --- /dev/null +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/TestData/ExampleModel.cs @@ -0,0 +1,16 @@ +namespace Altinn.App.Core.Infrastructure.Clients.Storage.TestData; + +/// +/// Example Model used in tests +/// +public class ExampleModel +{ + /// + /// The name + /// + public string Name { get; set; } = ""; + /// + /// The age + /// + public int Age { get; set; } = 0; +} diff --git a/src/Altinn.App.Core/Interface/IData.cs b/src/Altinn.App.Core/Interface/IData.cs index 0c19544a8..643dfd037 100644 --- a/src/Altinn.App.Core/Interface/IData.cs +++ b/src/Altinn.App.Core/Interface/IData.cs @@ -119,10 +119,21 @@ public interface IData /// The instance id /// The data id /// Http request containing the attachment to be saved + [Obsolete(message:"Deprecated please use UpdateBinaryData(InstanceIdentifier, string, string, Guid, Stream) instead", error: false)] Task UpdateBinaryData(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid, HttpRequest request); /// - /// Updates a binary data element. + /// Method that updates a form attachments to disk/storage and returns the updated data element. + /// + /// Instance identifier instanceOwnerPartyId and instanceGuid + /// Content type of the updated binary data + /// Filename of the updated binary data + /// Guid of the data element to update + /// Updated binary data + Task UpdateBinaryData(InstanceIdentifier instanceIdentifier, string? contentType, string filename, Guid dataGuid, Stream stream); + + /// + /// Insert a binary data element. /// /// isntanceId = {instanceOwnerPartyId}/{instanceGuid} /// data type diff --git a/test/Altinn.App.Core.Tests/Implementation/DataClientTest.cs b/test/Altinn.App.Core.Tests/Implementation/DataClientTest.cs deleted file mode 100644 index d76bb416c..000000000 --- a/test/Altinn.App.Core.Tests/Implementation/DataClientTest.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Altinn.App.Core.Configuration; -using Altinn.App.Core.Helpers; -using Altinn.App.Core.Infrastructure.Clients.Storage; -using Altinn.App.PlatformServices.Tests.Data; -using Altinn.App.PlatformServices.Tests.Mocks; -using Altinn.Platform.Storage.Interface.Models; - -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using Moq; - -using Xunit; - -namespace Altinn.App.PlatformServices.Tests.Implementation -{ - public class DataClientTest - { - private readonly Mock> platformSettingsOptions; - private readonly Mock> appSettingsOptions; - private readonly Mock contextAccessor; - private readonly Mock> logger; - - public DataClientTest() - { - platformSettingsOptions = new Mock>(); - PlatformSettings platformSettings = new() { ApiStorageEndpoint = "http://localhost/" }; - platformSettingsOptions.Setup(s => s.Value).Returns(platformSettings); - - appSettingsOptions = new Mock>(); - AppSettings appSettings = new() { RuntimeCookieName = "AltinnStudioRuntime" }; - appSettingsOptions.Setup(s => s.CurrentValue).Returns(appSettings); - - contextAccessor = new Mock(); - contextAccessor.Setup(s => s.HttpContext).Returns(new DefaultHttpContext()); - - logger = new Mock>(); - } - - [Fact] - public async Task InsertBinaryData_MethodProduceValidPlatformRequest() - { - // Arrange - HttpRequestMessage platformRequest = null; - DelegatingHandlerStub delegatingHandler = new(async (HttpRequestMessage request, CancellationToken token) => - { - platformRequest = request; - - DataElement dataElement = new DataElement - { - Id = "DataElement.Id", - InstanceGuid = "InstanceGuid" - }; - await Task.CompletedTask; - return new HttpResponseMessage() { Content = JsonContent.Create(dataElement) }; - }); - - Mock> generalSettingsOptions = new Mock>(); - var target = new DataClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - appSettingsOptions.Object, - new HttpClient(delegatingHandler)); - - var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); - - // Act - DataElement actual = await target.InsertBinaryData("instanceId", "catstories", "application/pdf", "a cats story.pdf", stream); - - // Assert - Assert.NotNull(actual); - - Assert.NotNull(platformRequest); - Assert.Equal(HttpMethod.Post, platformRequest.Method); - Assert.EndsWith("dataType=catstories", platformRequest.RequestUri.ToString()); - Assert.Equal("\"a cats story.pdf\"", platformRequest.Content.Headers.ContentDisposition.FileName); - } - - [Fact] - public async Task GetFormData_MethodProduceValidPlatformRequest_ReturnedFormIsValid() - { - // Arrange - HttpRequestMessage platformRequest = null; - DelegatingHandlerStub delegatingHandler = new(async (HttpRequestMessage request, CancellationToken token) => - { - platformRequest = request; - - string serializedModel = string.Empty - + @"" - + @"" - + @" " - + @" Test Test 123" - + @" " - + @""; - await Task.CompletedTask; - - HttpResponseMessage response = new HttpResponseMessage() { Content = new StringContent(serializedModel) }; - response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/xml"); - return response; - }); - - Mock> generalSettingsOptions = new Mock>(); - var target = new DataClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - appSettingsOptions.Object, - new HttpClient(delegatingHandler)); - - Guid dataElementGuid = Guid.NewGuid(); - - // Act - object response = await target.GetFormData(Guid.NewGuid(), typeof(SkjemaWithNamespace), "org", "app", 323413, dataElementGuid); - - // Assert - var actual = response as SkjemaWithNamespace; - Assert.NotNull(actual); - Assert.NotNull(actual.Foretakgrp8820); - Assert.NotNull(actual.Foretakgrp8820.EnhetNavnEndringdatadef31); - - Assert.NotNull(platformRequest); - Assert.Equal(HttpMethod.Get, platformRequest.Method); - Assert.EndsWith($"data/{dataElementGuid}", platformRequest.RequestUri.ToString()); - } - - [Fact] - public async Task InsertBinaryData_PlatformRespondNotOk_ThrowsPlatformException() - { - // Arrange - HttpRequestMessage platformRequest = null; - DelegatingHandlerStub delegatingHandler = new(async (HttpRequestMessage request, CancellationToken token) => - { - platformRequest = request; - - DataElement dataElement = new DataElement - { - Id = "DataElement.Id", - InstanceGuid = "InstanceGuid" - }; - await Task.CompletedTask; - return new HttpResponseMessage() { StatusCode = HttpStatusCode.BadRequest }; - }); - - Mock> generalSettingsOptions = new Mock>(); - var target = new DataClient( - platformSettingsOptions.Object, - logger.Object, - contextAccessor.Object, - appSettingsOptions.Object, - new HttpClient(delegatingHandler)); - - var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); - - PlatformHttpException actual = null; - - // Act - try - { - _ = await target.InsertBinaryData("instanceId", "catstories", "application/pdf", "a cats story.pdf", stream); - } - catch (PlatformHttpException phe) - { - actual = phe; - } - - // Assert - Assert.NotNull(actual); - Assert.Equal(HttpStatusCode.BadRequest, actual.Response.StatusCode); - } - } -} diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/DataClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/DataClientTests.cs new file mode 100644 index 000000000..6eae354da --- /dev/null +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/DataClientTests.cs @@ -0,0 +1,530 @@ +#nullable enable +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Infrastructure.Clients.Storage; +using Altinn.App.Core.Infrastructure.Clients.Storage.TestData; +using Altinn.App.Core.Interface; +using Altinn.App.Core.Models; +using Altinn.App.PlatformServices.Tests.Data; +using Altinn.App.PlatformServices.Tests.Mocks; +using Altinn.Platform.Storage.Interface.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.App.Core.Tests.Infrastructure.Clients +{ + public class DataClientTests + { + private readonly Mock> platformSettingsOptions; + private readonly Mock userTokenProvide; + private readonly ILogger logger; + private readonly string apiStorageEndpoint = "https://local.platform.altinn.no/api/storage/"; + + public DataClientTests() + { + platformSettingsOptions = new Mock>(); + PlatformSettings platformSettings = new() { ApiStorageEndpoint = apiStorageEndpoint }; + platformSettingsOptions.Setup(s => s.Value).Returns(platformSettings); + userTokenProvide = new Mock(); + userTokenProvide.Setup(u => u.GetUserToken()).Returns("dummytesttoken"); + + logger = new NullLogger(); + } + + [Fact] + public async Task InsertBinaryData_MethodProduceValidPlatformRequest() + { + // Arrange + HttpRequestMessage? platformRequest = null; + + var target = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + platformRequest = request; + + DataElement dataElement = new DataElement + { + Id = "DataElement.Id", + InstanceGuid = "InstanceGuid" + }; + await Task.CompletedTask; + return new HttpResponseMessage() { Content = JsonContent.Create(dataElement) }; + }); + + var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); + var instanceIdentifier = new InstanceIdentifier(323413, Guid.NewGuid()); + Uri expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data?dataType=catstories", UriKind.RelativeOrAbsolute); + + // Act + DataElement actual = await target.InsertBinaryData(instanceIdentifier.ToString(), "catstories", "application/pdf", "a cats story.pdf", stream); + + // Assert + Assert.NotNull(actual); + + Assert.NotNull(platformRequest); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Post, "\"a cats story.pdf\"", "application/pdf"); + } + + [Fact] + public async Task GetFormData_MethodProduceValidPlatformRequest_ReturnedFormIsValid() + { + // Arrange + HttpRequestMessage? platformRequest = null; + + var target = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + platformRequest = request; + + string serializedModel = string.Empty + + @"" + + @"" + + @" " + + @" Test Test 123" + + @" " + + @""; + await Task.CompletedTask; + + HttpResponseMessage response = new HttpResponseMessage() { Content = new StringContent(serializedModel) }; + response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/xml"); + return response; + }); + + Guid dataElementGuid = Guid.NewGuid(); + var instanceIdentifier = new InstanceIdentifier(323413, Guid.NewGuid()); + Uri expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataElementGuid}", UriKind.RelativeOrAbsolute); + + // Act + object response = await target.GetFormData(instanceIdentifier.InstanceGuid, typeof(SkjemaWithNamespace), "org", "app", 323413, dataElementGuid); + + // Assert + var actual = response as SkjemaWithNamespace; + Assert.NotNull(actual); + Assert.NotNull(actual!.Foretakgrp8820); + Assert.NotNull(actual!.Foretakgrp8820.EnhetNavnEndringdatadef31); + + Assert.NotNull(platformRequest); + AssertHttpRequest(platformRequest, expectedUri, HttpMethod.Get, null, "application/xml"); + } + + [Fact] + public async Task InsertBinaryData_PlatformRespondNotOk_ThrowsPlatformException() + { + // Arrange + var target = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.BadRequest }; + }); + + var stream = new MemoryStream(Encoding.UTF8.GetBytes("This is not a pdf, but no one here will care.")); + + // Act + var actual = await Assert.ThrowsAsync(async () => await target.InsertBinaryData("instanceId", "catstories", "application/pdf", "a cats story.pdf", stream)); + + // Assert + Assert.NotNull(actual); + Assert.Equal(HttpStatusCode.BadRequest, actual.Response.StatusCode); + } + + [Fact] + public async Task UpdateBinaryData_put_updated_data_and_Return_DataElement() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + DataElement expectedDataelement = new DataElement + { + Id = instanceIdentifier.ToString(), + InstanceGuid = instanceIdentifier.InstanceGuid.ToString() + }; + var dataClient = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; + + DataElement dataElement = new DataElement + { + Id = instanceIdentifier.ToString(), + InstanceGuid = instanceIdentifier.InstanceGuid.ToString() + }; + await Task.CompletedTask; + return new HttpResponseMessage() { Content = JsonContent.Create(dataElement) }; + }); + Uri expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", UriKind.RelativeOrAbsolute); + var restult = await dataClient.UpdateBinaryData(instanceIdentifier, "application/json", "test.json", dataGuid, new MemoryStream()); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, "test.json", "application/json"); + restult.Should().BeEquivalentTo(expectedDataelement); + } + + [Fact] + public async Task UpdateBinaryData_returns_exception_when_put_to_storage_result_in_servererror() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + var dataClient = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + }); + var actual = await Assert.ThrowsAsync(async () => await dataClient.UpdateBinaryData(instanceIdentifier, "application/json", "test.json", dataGuid, new MemoryStream())); + invocations.Should().Be(1); + actual.Should().NotBeNull(); + actual.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + + [Fact] + public async Task UpdateBinaryData_returns_exception_when_put_to_storage_result_in_conflict() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + var dataClient = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.Conflict }; + }); + var actual = await Assert.ThrowsAsync(async () => await dataClient.UpdateBinaryData(instanceIdentifier, "application/json", "test.json", dataGuid, new MemoryStream())); + invocations.Should().Be(1); + actual.Should().NotBeNull(); + actual.Response.StatusCode.Should().Be(HttpStatusCode.Conflict); + } + + [Fact] + public async Task GetBinaryData_returns_stream_of_binary_data() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; + + await Task.CompletedTask; + return new HttpResponseMessage() { Content = new StringContent("hello worlds") }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", UriKind.RelativeOrAbsolute); + var response = await dataClient.GetBinaryData("ttd", "app", instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, dataGuid); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Get, null, null); + using StreamReader streamReader = new StreamReader(response); + var responseString = await streamReader.ReadToEndAsync(); + responseString.Should().BeEquivalentTo("hello worlds"); + } + + [Fact] + public async Task GetBinaryData_returns_empty_stream_when_storage_returns_notfound() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; + + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", UriKind.RelativeOrAbsolute); + var response = await dataClient.GetBinaryData("ttd", "app", instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, dataGuid); + response.Should().BeNull(); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Get, null, null); + } + + [Fact] + public async Task GetBinaryData_throws_PlatformHttpException_when_server_error_returned_from_storage() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + var dataClient = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + }); + var actual = await Assert.ThrowsAsync(async () => await dataClient.GetBinaryData("ttd", "app", instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, dataGuid)); + invocations.Should().Be(1); + actual.Should().NotBeNull(); + actual.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + + [Fact] + public async Task GetBinaryDataList_returns_AttachemtList_when_DataElements_found() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; + + await Task.CompletedTask; + return new HttpResponseMessage() + { Content = new StringContent("{\"dataElements\":[{\"Id\":\"aaaa-bbbb-cccc-dddd\",\"Size\":10,\"DataType\":\"cats\"},{\"Id\":\"eeee-ffff-gggg-hhhh\", \"Size\":20,\"DataType\":\"dogs\"}]}") }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/dataelements", UriKind.RelativeOrAbsolute); + var response = await dataClient.GetBinaryDataList("ttd", "app", instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Get, null, null); + + var expectedList = new List() + { + new AttachmentList() + { + Attachments = new List() + { + new Attachment() + { + Id = "aaaa-bbbb-cccc-dddd", + Size = 10 + } + }, + Type = "cats" + }, + new AttachmentList() + { + Attachments = new List() + { + new Attachment() + { + Id = "eeee-ffff-gggg-hhhh", + Size = 20 + } + }, + Type = "dogs" + }, + new AttachmentList() + { + Attachments = new List() + { + new Attachment() + { + Id = "eeee-ffff-gggg-hhhh", + Size = 20 + } + }, + Type = "attachments" + }, + }; + response.Should().BeEquivalentTo(expectedList); + } + + [Fact] + public async Task GetBinaryDataList_throws_PlatformHttpException_if_non_ok_response() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + int invocations = 0; + var dataClient = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + }); + var actual = await Assert.ThrowsAsync(async () => await dataClient.GetBinaryDataList("ttd", "app", instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid)); + invocations.Should().Be(1); + actual.Should().NotBeNull(); + actual.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + + [Fact] + public async Task DeleteBinaryData_returns_true_when_data_was_deleted() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; + + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}?delay=False", UriKind.RelativeOrAbsolute); + var result = await dataClient.DeleteBinaryData("ttd", "app", instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, dataGuid); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + result.Should().BeTrue(); + } + + [Fact] + public async Task DeleteBinaryData_throws_PlatformHttpException_when_dataelement_not_found() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; + + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}?delay=False", UriKind.RelativeOrAbsolute); + var actual = await Assert.ThrowsAsync(async () => await dataClient.DeleteBinaryData("ttd", "app", instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, dataGuid)); + invocations.Should().Be(1); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + actual.Response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [Fact] + public async Task DeleteData_returns_true_when_data_was_deleted_with_delay_true() + { + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient(async (HttpRequestMessage request, CancellationToken token) => + { + invocations++; + platformRequest = request; + + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}?delay=True", UriKind.RelativeOrAbsolute); + var result = await dataClient.DeleteData("ttd", "app", instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, dataGuid, true); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Delete); + result.Should().BeTrue(); + } + + [Fact] + public async Task UpdateData_serializes_and_updates_formdata() + { + ExampleModel exampleModel = new ExampleModel() + { + Name = "Test", + Age = 22 + }; + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient(async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", UriKind.RelativeOrAbsolute); + await dataClient.UpdateData(exampleModel, instanceIdentifier.InstanceGuid, exampleModel.GetType(), "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, dataGuid); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, null, "application/xml"); + } + + [Fact] + public async Task UpdateData_throws_error_if_serilization_fails() + { + object exampleModel = new ExampleModel() + { + Name = "Test", + Age = 22 + }; + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + int invocations = 0; + var dataClient = GetDataClient(async (request, token) => + { + invocations++; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK }; + }); + await Assert.ThrowsAsync(async () => await dataClient.UpdateData(exampleModel, instanceIdentifier.InstanceGuid, typeof(DataElement), "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, dataGuid)); + invocations.Should().Be(0); + } + + [Fact] + public async Task UpdateData_throws_platformhttpexception_if_platform_request_fails() + { + object exampleModel = new ExampleModel() + { + Name = "Test", + Age = 22 + }; + var instanceIdentifier = new InstanceIdentifier("501337/d3f3250d-705c-4683-a215-e05ebcbe6071"); + var dataGuid = new Guid("67a5ef12-6e38-41f8-8b42-f91249ebcec0"); + HttpRequestMessage? platformRequest = null; + int invocations = 0; + var dataClient = GetDataClient(async (request, token) => + { + invocations++; + platformRequest = request; + await Task.CompletedTask; + return new HttpResponseMessage() { StatusCode = HttpStatusCode.InternalServerError }; + }); + var expectedUri = new Uri($"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", UriKind.RelativeOrAbsolute); + var result = await Assert.ThrowsAsync(async () => await dataClient.UpdateData(exampleModel, instanceIdentifier.InstanceGuid, typeof(ExampleModel), "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, dataGuid)); + invocations.Should().Be(1); + platformRequest?.Should().NotBeNull(); + AssertHttpRequest(platformRequest!, expectedUri, HttpMethod.Put, null, "application/xml"); + result.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + + private DataClient GetDataClient(Func> handlerFunc) + { + DelegatingHandlerStub delegatingHandlerStub = new(handlerFunc); + return new DataClient( + platformSettingsOptions.Object, + logger, + new HttpClient(delegatingHandlerStub), + userTokenProvide.Object); + } + + private void AssertHttpRequest(HttpRequestMessage actual, Uri expectedUri, HttpMethod method, string? expectedFilename = null, string? expectedContentType = null) + { + IEnumerable? actualContentType = null; + IEnumerable? actualContentDisposition = null; + actual.Content?.Headers.TryGetValues("Content-Type", out actualContentType); + actual.Content?.Headers.TryGetValues("Content-Disposition", out actualContentDisposition); + var authHeader = actual.Headers.Authorization; + actual.RequestUri.Should().BeEquivalentTo(expectedUri); + Uri.Compare(actual.RequestUri, expectedUri, UriComponents.HttpRequestUrl, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase).Should().Be(0, "Actual request Uri did not match expected Uri"); + if (expectedContentType is not null) + { + actualContentType?.FirstOrDefault().Should().BeEquivalentTo(expectedContentType); + } + + if (expectedFilename is not null) + { + ContentDispositionHeaderValue.Parse(actualContentDisposition?.FirstOrDefault()).FileName?.Should().BeEquivalentTo(expectedFilename); + } + + authHeader?.Parameter.Should().BeEquivalentTo("dummytesttoken"); + } + } +}