diff --git a/src/Altinn.App.Core/Extensions/HttpClientExtension.cs b/src/Altinn.App.Core/Extensions/HttpClientExtension.cs index 541ab9db2..b33864c79 100644 --- a/src/Altinn.App.Core/Extensions/HttpClientExtension.cs +++ b/src/Altinn.App.Core/Extensions/HttpClientExtension.cs @@ -103,6 +103,36 @@ public static async Task GetAsync( return await httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, CancellationToken.None); } + /// + /// Extension that add authorization header to request + /// + /// The HttpClient + /// the authorization token (jwt) + /// The request Uri + /// The platformAccess tokens + /// A HttpResponseMessage + public static async Task GetStreamingAsync( + this HttpClient httpClient, + string authorizationToken, + string requestUri, + string? platformAccessToken = null + ) + { + using HttpRequestMessage request = new(HttpMethod.Get, requestUri); + + request.Headers.Authorization = new AuthenticationHeaderValue( + Constants.AuthorizationSchemes.Bearer, + authorizationToken + ); + + if (!string.IsNullOrEmpty(platformAccessToken)) + { + request.Headers.Add(Constants.General.PlatformAccessTokenHeaderName, platformAccessToken); + } + + return await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); + } + /// /// Extension that add authorization header to request /// diff --git a/src/Altinn.App.Core/Features/Correspondence/Builder/CorrespondenceAttachmentBuilder.cs b/src/Altinn.App.Core/Features/Correspondence/Builder/CorrespondenceAttachmentBuilder.cs index c659e5363..11495d526 100644 --- a/src/Altinn.App.Core/Features/Correspondence/Builder/CorrespondenceAttachmentBuilder.cs +++ b/src/Altinn.App.Core/Features/Correspondence/Builder/CorrespondenceAttachmentBuilder.cs @@ -10,6 +10,7 @@ public class CorrespondenceAttachmentBuilder : ICorrespondenceAttachmentBuilder private string? _filename; private string? _sendersReference; private ReadOnlyMemory? _data; + private Stream? _streamedData; private bool? _isEncrypted; private CorrespondenceDataLocationType _dataLocationType = CorrespondenceDataLocationType.ExistingCorrespondenceAttachment; @@ -40,11 +41,17 @@ public ICorrespondenceAttachmentBuilderData WithSendersReference(string sendersR /// public ICorrespondenceAttachmentBuilder WithData(ReadOnlyMemory data) { - BuilderUtils.NotNullOrEmpty(data, "Data cannot be empty"); _data = data; return this; } + /// + public ICorrespondenceAttachmentBuilder WithData(Stream data) + { + _streamedData = data; + return this; + } + /// public ICorrespondenceAttachmentBuilder WithIsEncrypted(bool isEncrypted) { @@ -64,15 +71,31 @@ public CorrespondenceAttachment Build() { BuilderUtils.NotNullOrEmpty(_filename); BuilderUtils.NotNullOrEmpty(_sendersReference); - BuilderUtils.NotNullOrEmpty(_data); + BuilderUtils.RequireExactlyOneOf(_data, _streamedData); - return new CorrespondenceAttachment + if (_streamedData is not null) + { + BuilderUtils.NotNullOrEmpty(_streamedData); + return new CorrespondenceStreamedAttachment + { + Filename = _filename, + SendersReference = _sendersReference, + Data = _streamedData, + IsEncrypted = _isEncrypted, + DataLocationType = _dataLocationType, + }; + } + else { - Filename = _filename, - SendersReference = _sendersReference, - Data = _data.Value, - IsEncrypted = _isEncrypted, - DataLocationType = _dataLocationType, - }; + BuilderUtils.NotNullOrEmpty(_data); + return new CorrespondenceAttachmentInMemory + { + Filename = _filename, + SendersReference = _sendersReference, + Data = _data.Value, + IsEncrypted = _isEncrypted, + DataLocationType = _dataLocationType, + }; + } } } diff --git a/src/Altinn.App.Core/Features/Correspondence/Builder/ICorrespondenceAttachmentBuilder.cs b/src/Altinn.App.Core/Features/Correspondence/Builder/ICorrespondenceAttachmentBuilder.cs index d8b7a7cd5..213d5aa83 100644 --- a/src/Altinn.App.Core/Features/Correspondence/Builder/ICorrespondenceAttachmentBuilder.cs +++ b/src/Altinn.App.Core/Features/Correspondence/Builder/ICorrespondenceAttachmentBuilder.cs @@ -27,7 +27,7 @@ public interface ICorrespondenceAttachmentBuilderSendersReference } /// -/// Indicates that the instance is on the step. +/// Indicates that the instance is on the and step. /// public interface ICorrespondenceAttachmentBuilderData { @@ -36,6 +36,15 @@ public interface ICorrespondenceAttachmentBuilderData /// /// The data ICorrespondenceAttachmentBuilder WithData(ReadOnlyMemory data); + + /// + /// Sets the stream of the data content of the attachment. + /// Is more efficient if the attachment is large in size. + /// The stream must be open (not disposed) until the correspondence is sent. + /// The caller is responsible for disposing the stream after the correspondence has been sent. + /// + /// The data stream + ICorrespondenceAttachmentBuilder WithData(Stream data); } /// diff --git a/src/Altinn.App.Core/Features/Correspondence/CorrespondenceClient.cs b/src/Altinn.App.Core/Features/Correspondence/CorrespondenceClient.cs index 78e1024b3..2ec9d0d93 100644 --- a/src/Altinn.App.Core/Features/Correspondence/CorrespondenceClient.cs +++ b/src/Altinn.App.Core/Features/Correspondence/CorrespondenceClient.cs @@ -191,6 +191,11 @@ CancellationToken cancellationToken ) { using HttpClient client = _httpClientFactory.CreateClient(); + + // Configure HttpClient for large file uploads + client.Timeout = TimeSpan.FromMinutes(30); + client.DefaultRequestHeaders.ExpectContinue = false; + using HttpResponseMessage response = await client.SendAsync(request, cancellationToken); string responseBody = await response.Content.ReadAsStringAsync(cancellationToken); diff --git a/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceAttachment.cs b/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceAttachment.cs index 52aed94ea..0efcb68f7 100644 --- a/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceAttachment.cs +++ b/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceAttachment.cs @@ -1,9 +1,9 @@ namespace Altinn.App.Core.Features.Correspondence.Models; /// -/// Represents an attachment to a correspondence. +/// Represents a base attachment of a correspondence. /// -public sealed record CorrespondenceAttachment : MultipartCorrespondenceItem +public abstract record CorrespondenceAttachment : MultipartCorrespondenceItem { /// /// The filename of the attachment. @@ -27,20 +27,7 @@ public sealed record CorrespondenceAttachment : MultipartCorrespondenceItem CorrespondenceDataLocationType.ExistingCorrespondenceAttachment; /// - /// The data content. + /// Serialise method /// - public required ReadOnlyMemory Data { get; init; } - - internal void Serialise(MultipartFormDataContent content, int index, string? filenameOverride = null) - { - const string typePrefix = "Correspondence.Content.Attachments"; - string prefix = $"{typePrefix}[{index}]"; - string actualFilename = filenameOverride ?? Filename; - - AddRequired(content, actualFilename, $"{prefix}.Filename"); - AddRequired(content, SendersReference, $"{prefix}.SendersReference"); - AddRequired(content, DataLocationType.ToString(), $"{prefix}.DataLocationType"); - AddRequired(content, Data, "Attachments", actualFilename); // NOTE: No prefix! - AddIfNotNull(content, IsEncrypted?.ToString(), $"{prefix}.IsEncrypted"); - } + internal abstract void Serialise(MultipartFormDataContent content, int index, string? filenameOverride = null); } diff --git a/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceAttachmentInMemory.cs b/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceAttachmentInMemory.cs new file mode 100644 index 000000000..d3495308a --- /dev/null +++ b/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceAttachmentInMemory.cs @@ -0,0 +1,25 @@ +namespace Altinn.App.Core.Features.Correspondence.Models; + +/// +/// Represents an attachment to a correspondence. +/// +public record CorrespondenceAttachmentInMemory : CorrespondenceAttachment +{ + /// + /// The data content. + /// + public required ReadOnlyMemory Data { get; init; } + + internal override void Serialise(MultipartFormDataContent content, int index, string? filenameOverride = null) + { + const string typePrefix = "Correspondence.Content.Attachments"; + string prefix = $"{typePrefix}[{index}]"; + string actualFilename = filenameOverride ?? Filename; + + AddRequired(content, actualFilename, $"{prefix}.Filename"); + AddRequired(content, SendersReference, $"{prefix}.SendersReference"); + AddRequired(content, DataLocationType.ToString(), $"{prefix}.DataLocationType"); + AddRequired(content, Data, "Attachments", actualFilename); + AddIfNotNull(content, IsEncrypted?.ToString(), $"{prefix}.IsEncrypted"); + } +} diff --git a/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceRequest.cs b/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceRequest.cs index a0f4e5fd2..170af0189 100644 --- a/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceRequest.cs +++ b/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceRequest.cs @@ -41,6 +41,13 @@ string filename content.Add(new ReadOnlyMemoryContent(data), name, filename); } + internal static void AddRequired(MultipartFormDataContent content, Stream data, string name, string filename) + { + if (data is null) + throw new CorrespondenceArgumentException($"Required value is missing: {name}"); + content.Add(new StreamContent(data), name, filename); + } + internal static void AddIfNotNull(MultipartFormDataContent content, string? value, string name) { if (!string.IsNullOrWhiteSpace(value)) diff --git a/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceStreamedAttachment.cs b/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceStreamedAttachment.cs new file mode 100644 index 000000000..01dccf3c3 --- /dev/null +++ b/src/Altinn.App.Core/Features/Correspondence/Models/CorrespondenceStreamedAttachment.cs @@ -0,0 +1,32 @@ +namespace Altinn.App.Core.Features.Correspondence.Models; + +/// +/// Represents an attachment to a correspondence with streaming data support. +/// Inherits from CorrespondenceAttachment and provides a Stream-based data property. +/// Is more efficient if the attachment is large in size. +/// The stream must be open (not disposed) until the correspondence is sent. +/// The caller is responsible for disposing the stream after the correspondence has been sent. +/// +public record CorrespondenceStreamedAttachment : CorrespondenceAttachment +{ + /// + /// The data content as a stream. + /// Is more efficient if the attachment is large in size. + /// The stream must be open (not disposed) until the correspondence is sent. + /// The caller is responsible for disposing the stream after the correspondence has been sent. + /// + public required Stream Data { get; init; } + + internal override void Serialise(MultipartFormDataContent content, int index, string? filenameOverride = null) + { + const string typePrefix = "Correspondence.Content.Attachments"; + string prefix = $"{typePrefix}[{index}]"; + string actualFilename = filenameOverride ?? Filename; + + AddRequired(content, actualFilename, $"{prefix}.Filename"); + AddRequired(content, SendersReference, $"{prefix}.SendersReference"); + AddRequired(content, DataLocationType.ToString(), $"{prefix}.DataLocationType"); + AddRequired(content, Data, "Attachments", actualFilename); + AddIfNotNull(content, IsEncrypted?.ToString(), $"{prefix}.IsEncrypted"); + } +} diff --git a/src/Altinn.App.Core/Helpers/ResponseWrapperStream.cs b/src/Altinn.App.Core/Helpers/ResponseWrapperStream.cs new file mode 100644 index 000000000..9e8e50f9b --- /dev/null +++ b/src/Altinn.App.Core/Helpers/ResponseWrapperStream.cs @@ -0,0 +1,114 @@ +namespace Altinn.App.Core.Helpers; + +/// +/// A wrapper stream that ensures proper disposal of an HttpResponseMessage along with its content stream. +/// +internal sealed class ResponseWrapperStream : Stream +{ + private readonly HttpResponseMessage _response; + private readonly Stream _innerStream; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP response message to be disposed when the stream is disposed. + /// The inner stream to wrap and delegate operations to. + public ResponseWrapperStream(HttpResponseMessage response, Stream innerStream) + { + _response = response; + _innerStream = innerStream; + } + + /// + /// Releases the unmanaged resources used by the and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + if (disposing) + { + _response?.Dispose(); // This will also dispose the inner stream + } + base.Dispose(disposing); + } + + // Delegate all Stream operations to _innerStream + + /// + /// Gets a value indicating whether the current stream supports reading. + /// + public override bool CanRead => _innerStream.CanRead; + + /// + /// Gets a value indicating whether the current stream supports seeking. + /// + public override bool CanSeek => _innerStream.CanSeek; + + /// + /// Gets a value indicating whether the current stream supports writing. + /// + public override bool CanWrite => _innerStream.CanWrite; + + /// + /// Gets the length in bytes of the stream. + /// + /// The stream does not support seeking. + public override long Length => _innerStream.Length; + + /// + /// Gets or sets the position within the current stream. + /// + /// The stream does not support seeking. + public override long Position + { + get => _innerStream.Position; + set => _innerStream.Position = value; + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. + /// + public override void Flush() => _innerStream.Flush(); + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// buffer is null. + /// offset or count is negative. + /// The sum of offset and count is larger than the buffer length. + /// The stream does not support reading. + public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); + + /// + /// Sets the position within the current stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type indicating the reference point used to obtain the new position. + /// The new position within the current stream. + /// The stream does not support seeking. + public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); + + /// + /// Sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + /// The stream does not support both writing and seeking. + /// value is negative. + public override void SetLength(long value) => _innerStream.SetLength(value); + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + /// buffer is null. + /// offset or count is negative. + /// The sum of offset and count is greater than the buffer length. + /// The stream does not support writing. + public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count); +} diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs index 6881b9c23..0867e42dd 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/DataClient.cs @@ -179,14 +179,25 @@ Guid dataId string token = _userTokenProvider.GetUserToken(); - HttpResponseMessage response = await _client.GetAsync(token, apiUrl); + HttpResponseMessage response = await _client.GetStreamingAsync(token, apiUrl); if (response.IsSuccessStatusCode) { - return await response.Content.ReadAsStreamAsync(); + Stream? stream = null; + try + { + stream = await response.Content.ReadAsStreamAsync(); + return new ResponseWrapperStream(response, stream); + } + catch (Exception) + { + response.Dispose(); + throw; + } } else if (response.StatusCode == HttpStatusCode.NotFound) { + response.Dispose(); #nullable disable return null; #nullable restore diff --git a/src/Altinn.App.Core/Internal/Data/DataService.cs b/src/Altinn.App.Core/Internal/Data/DataService.cs index bbe27960a..4159e22e6 100644 --- a/src/Altinn.App.Core/Internal/Data/DataService.cs +++ b/src/Altinn.App.Core/Internal/Data/DataService.cs @@ -110,7 +110,7 @@ private async Task GetDataForDataElement(InstanceIdentifier instanceIdenti { ApplicationMetadata applicationMetadata = await _appMetadata.GetApplicationMetadata(); - Stream dataStream = await _dataClient.GetBinaryData( + using Stream dataStream = await _dataClient.GetBinaryData( applicationMetadata.AppIdentifier.Org, applicationMetadata.AppIdentifier.App, instanceIdentifier.InstanceOwnerPartyId, diff --git a/test/Altinn.App.Core.Tests/Features/Correspondence/Builder/CorrespondenceBuilderTests.cs b/test/Altinn.App.Core.Tests/Features/Correspondence/Builder/CorrespondenceBuilderTests.cs index afdcb2104..b13877ed8 100644 --- a/test/Altinn.App.Core.Tests/Features/Correspondence/Builder/CorrespondenceBuilderTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Correspondence/Builder/CorrespondenceBuilderTests.cs @@ -195,7 +195,7 @@ public void Build_WithAllProperties_ShouldReturnValidCorrespondence() .WithIsEncrypted(data.attachments[0].isEncrypted) ) .WithAttachment( - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = data.attachments[1].filename, SendersReference = data.attachments[1].sendersReference, @@ -206,7 +206,7 @@ public void Build_WithAllProperties_ShouldReturnValidCorrespondence() ) .WithAttachments( [ - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = data.attachments[2].filename, SendersReference = data.attachments[2].sendersReference, @@ -273,10 +273,8 @@ public void Build_WithAllProperties_ShouldReturnValidCorrespondence() correspondence.Content.Attachments[i].IsEncrypted.Should().Be(data.attachments[i].isEncrypted); correspondence.Content.Attachments[i].SendersReference.Should().Be(data.attachments[i].sendersReference); correspondence.Content.Attachments[i].DataLocationType.Should().Be(data.attachments[i].dataLocationType); - Encoding - .UTF8.GetString(correspondence.Content.Attachments[i].Data.Span) - .Should() - .Be(data.attachments[i].data); + var attachment = correspondence.Content.Attachments[i] as CorrespondenceAttachmentInMemory; + Encoding.UTF8.GetString(attachment!.Data.Span).Should().Be(data.attachments[i].data); } correspondence.Notification.NotificationTemplate.Should().Be(data.notification.template); diff --git a/test/Altinn.App.Core.Tests/Features/Correspondence/Models/CorrespondenceRequestTests.cs b/test/Altinn.App.Core.Tests/Features/Correspondence/Models/CorrespondenceRequestTests.cs index fc235e383..7daf63508 100644 --- a/test/Altinn.App.Core.Tests/Features/Correspondence/Models/CorrespondenceRequestTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Correspondence/Models/CorrespondenceRequestTests.cs @@ -38,13 +38,13 @@ public async Task Serialise_ShouldAddCorrectFields() Language = LanguageCode.Parse("no"), Attachments = [ - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = "filename-1", SendersReference = "senders-reference-1", Data = "data"u8.ToArray(), }, - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = "filename-2", SendersReference = "senders-reference-2", @@ -208,13 +208,13 @@ public async Task Serialise_ShouldAddCorrectFields_IsReservedOverridesIgnoreRese Language = LanguageCode.Parse("no"), Attachments = [ - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = "filename-1", SendersReference = "senders-reference-1", Data = "data"u8.ToArray(), }, - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = "filename-2", SendersReference = "senders-reference-2", @@ -372,13 +372,13 @@ public async Task Serialise_ShouldHandleClashingFilenames(string clashingFilenam Language = LanguageCode.Parse("no"), Attachments = [ - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = clashingFilename, SendersReference = "senders-reference-1", Data = Encoding.UTF8.GetBytes("data-1"), }, - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = clashingFilename, SendersReference = "senders-reference-2", @@ -401,25 +401,25 @@ public void Serialise_ClashingFilenames_ShouldUseReferenceComparison() { // Arrange ReadOnlyMemory data = Encoding.UTF8.GetBytes("data"); - List identicalAttachments = + List identicalAttachments = [ - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = "filename", SendersReference = "senders-reference", - Data = data, + Data = data.ToArray(), }, - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = "filename", SendersReference = "senders-reference", - Data = data, + Data = data.ToArray(), }, - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = "filename", SendersReference = "senders-reference", - Data = data, + Data = data.ToArray(), }, ]; var clonedAttachment = identicalAttachments[^1]; diff --git a/test/Altinn.App.Core.Tests/Features/Correspondence/Models/CorrespondenceRequestTests_Obsolete.cs b/test/Altinn.App.Core.Tests/Features/Correspondence/Models/CorrespondenceRequestTests_Obsolete.cs index bc20cc8eb..09be6ed6a 100644 --- a/test/Altinn.App.Core.Tests/Features/Correspondence/Models/CorrespondenceRequestTests_Obsolete.cs +++ b/test/Altinn.App.Core.Tests/Features/Correspondence/Models/CorrespondenceRequestTests_Obsolete.cs @@ -39,13 +39,13 @@ public async Task Serialise_ShouldAddCorrectFields() Language = LanguageCode.Parse("no"), Attachments = [ - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = "filename-1", SendersReference = "senders-reference-1", Data = "data"u8.ToArray(), }, - new CorrespondenceAttachment + new CorrespondenceAttachmentInMemory { Filename = "filename-2", SendersReference = "senders-reference-2", diff --git a/test/Altinn.App.Core.Tests/Features/Signing/SigningReceiptServiceTests.cs b/test/Altinn.App.Core.Tests/Features/Signing/SigningReceiptServiceTests.cs index 75d160926..5de691b62 100644 --- a/test/Altinn.App.Core.Tests/Features/Signing/SigningReceiptServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Signing/SigningReceiptServiceTests.cs @@ -431,7 +431,8 @@ public async Task GetCorrespondenceAttachments_ReturnsCorrectAttachments() // Assert Assert.Single(attachments); - CorrespondenceAttachment attachment = attachments.First(); + var attachment = attachments.First() as CorrespondenceAttachmentInMemory; + Assert.NotNull(attachment); Assert.Equal("signed.pdf", attachment.Filename); Assert.Equal(signedElement.Id, attachment.SendersReference); Assert.Equal(new byte[] { 1, 2, 3 }, attachment.Data); 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 2567ceb18..fe1bbc52c 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Storage/DataClientTests.cs @@ -331,7 +331,7 @@ public async Task GetBinaryData_returns_stream_of_binary_data() $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", UriKind.RelativeOrAbsolute ); - var response = await dataClient.GetBinaryData( + using var response = await dataClient.GetBinaryData( "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, @@ -367,7 +367,7 @@ public async Task GetBinaryData_returns_empty_stream_when_storage_returns_notfou $"{apiStorageEndpoint}instances/{instanceIdentifier}/data/{dataGuid}", UriKind.RelativeOrAbsolute ); - var response = await dataClient.GetBinaryData( + using var response = await dataClient.GetBinaryData( "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, @@ -396,14 +396,15 @@ public async Task GetBinaryData_throws_PlatformHttpException_when_server_error_r } ); var actual = await Assert.ThrowsAsync(async () => - await dataClient.GetBinaryData( + { + using var _ = await dataClient.GetBinaryData( "ttd", "app", instanceIdentifier.InstanceOwnerPartyId, instanceIdentifier.InstanceGuid, dataGuid - ) - ); + ); + }); invocations.Should().Be(1); actual.Should().NotBeNull(); actual.Response.StatusCode.Should().Be(HttpStatusCode.InternalServerError); 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 88ab09735..6dd3950aa 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 @@ -232,6 +232,7 @@ namespace Altinn.App.Core.Extensions { public static System.Threading.Tasks.Task DeleteAsync(this System.Net.Http.HttpClient httpClient, string authorizationToken, string requestUri, string? platformAccessToken = null) { } public static System.Threading.Tasks.Task GetAsync(this System.Net.Http.HttpClient httpClient, string authorizationToken, string requestUri, string? platformAccessToken = null) { } + public static System.Threading.Tasks.Task GetStreamingAsync(this System.Net.Http.HttpClient httpClient, string authorizationToken, string requestUri, string? platformAccessToken = null) { } public static System.Threading.Tasks.Task PostAsync(this System.Net.Http.HttpClient httpClient, string authorizationToken, string requestUri, System.Net.Http.HttpContent? content, string? platformAccessToken = null) { } public static System.Threading.Tasks.Task PutAsync(this System.Net.Http.HttpClient httpClient, string authorizationToken, string requestUri, System.Net.Http.HttpContent? content, string? platformAccessToken = null) { } } @@ -428,6 +429,7 @@ namespace Altinn.App.Core.Features.Correspondence.Builder public class CorrespondenceAttachmentBuilder : Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilder, Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilderData, Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilderFilename, Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilderSendersReference { public Altinn.App.Core.Features.Correspondence.Models.CorrespondenceAttachment Build() { } + public Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilder WithData(System.IO.Stream data) { } public Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilder WithData(System.ReadOnlyMemory data) { } public Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilder WithDataLocationType(Altinn.App.Core.Features.Correspondence.Models.CorrespondenceDataLocationType dataLocationType) { } public Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilderSendersReference WithFilename(string filename) { } @@ -546,6 +548,7 @@ namespace Altinn.App.Core.Features.Correspondence.Builder } public interface ICorrespondenceAttachmentBuilderData { + Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilder WithData(System.IO.Stream data); Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilder WithData(System.ReadOnlyMemory data); } public interface ICorrespondenceAttachmentBuilderFilename @@ -721,15 +724,19 @@ namespace Altinn.App.Core.Features.Correspondence } namespace Altinn.App.Core.Features.Correspondence.Models { - public sealed class CorrespondenceAttachment : Altinn.App.Core.Features.Correspondence.Models.MultipartCorrespondenceItem, System.IEquatable + public abstract class CorrespondenceAttachment : Altinn.App.Core.Features.Correspondence.Models.MultipartCorrespondenceItem, System.IEquatable { - public CorrespondenceAttachment() { } - public required System.ReadOnlyMemory Data { get; init; } + protected CorrespondenceAttachment() { } public Altinn.App.Core.Features.Correspondence.Models.CorrespondenceDataLocationType DataLocationType { get; init; } public required string Filename { get; init; } public bool? IsEncrypted { get; init; } public required string SendersReference { get; init; } } + public class CorrespondenceAttachmentInMemory : Altinn.App.Core.Features.Correspondence.Models.CorrespondenceAttachment, System.IEquatable + { + public CorrespondenceAttachmentInMemory() { } + public required System.ReadOnlyMemory Data { get; init; } + } public sealed class CorrespondenceAttachmentResponse : System.IEquatable { public CorrespondenceAttachmentResponse() { } @@ -1042,6 +1049,11 @@ namespace Altinn.App.Core.Features.Correspondence.Models [System.Text.Json.Serialization.JsonPropertyName("statusText")] public required string StatusText { get; init; } } + public class CorrespondenceStreamedAttachment : Altinn.App.Core.Features.Correspondence.Models.CorrespondenceAttachment, System.IEquatable + { + public CorrespondenceStreamedAttachment() { } + public required System.IO.Stream Data { get; init; } + } public sealed class GetCorrespondenceStatusPayload : Altinn.App.Core.Features.Correspondence.Models.CorrespondencePayloadBase, System.IEquatable { public GetCorrespondenceStatusPayload(System.Guid correspondenceId, Altinn.App.Core.Features.Correspondence.Models.CorrespondenceAuthorisation authorisation) { } diff --git a/test/Altinn.App.Integration.Tests/Basic/_snapshots/BasicAppTests.Full_5_PDF.verified.txt b/test/Altinn.App.Integration.Tests/Basic/_snapshots/BasicAppTests.Full_5_PDF.verified.txt index 9d65fbfd3..1b98f7e4b 100644 --- a/test/Altinn.App.Integration.Tests/Basic/_snapshots/BasicAppTests.Full_5_PDF.verified.txt +++ b/test/Altinn.App.Integration.Tests/Basic/_snapshots/BasicAppTests.Full_5_PDF.verified.txt @@ -3,14 +3,14 @@ Version: 1.1, Content: { Headers: { - Content-Length: [ - - ], Content-Type: [ application/pdf ], Content-Disposition: [ attachment; filename=basic.pdf; filename*=UTF-8''basic.pdf + ], + Content-Length: [ + ] } }, @@ -24,6 +24,9 @@ Cache-Control: [ no-store, no-cache ], + Transfer-Encoding: [ + chunked + ], Request-Context: [ appId= ],