From ac26d2b5f63f14fd7cb0b7766a7855bb37bf601a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Jan 2025 13:59:42 +0000 Subject: [PATCH] Added FileUploadHandlingMiddleware for processing larger than allowed files. --- .../FileUploadMiddlewareTests.cs | 88 +++++++++++++++++++ .../FileUploadMiddleware.cs | 25 ++++++ .../Pages/Forms/FormElementFileUploadModel.cs | 2 +- Frontend/CO.CDP.OrganisationApp/Program.cs | 21 +++-- 4 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 Frontend/CO.CDP.OrganisationApp.Tests/FileUploadMiddlewareTests.cs create mode 100644 Frontend/CO.CDP.OrganisationApp/FileUploadMiddleware.cs diff --git a/Frontend/CO.CDP.OrganisationApp.Tests/FileUploadMiddlewareTests.cs b/Frontend/CO.CDP.OrganisationApp.Tests/FileUploadMiddlewareTests.cs new file mode 100644 index 000000000..93aa85456 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp.Tests/FileUploadMiddlewareTests.cs @@ -0,0 +1,88 @@ +using CO.CDP.OrganisationApp.Pages.Forms; +using Microsoft.AspNetCore.Http; +using Moq; +using System.Net; + +namespace CO.CDP.OrganisationApp.Tests; + +public class FileUploadMiddlewareTests +{ + public class FileUploadMiddlewareTest : FileUploadMiddleware + { + public FileUploadMiddlewareTest(RequestDelegate next) : base(next) + { + } + + protected override async Task WriteAsync(HttpContext context) + { + var writer = new StreamWriter(context.Response.Body); + + await writer.WriteAsync($"The uploaded file is too large. Maximum allowed size is {FormElementFileUploadModel.AllowedMaxFileSizeMB} MB."); + writer.Flush(); + } + } + + [Fact] + public async Task InvokeAsync_ShouldWriteResponse_WhenStatusCodeIs400AndContentTypeIsMultipartFormData() + { + var responseBody = new MemoryStream(); + var httpContextMock = new Mock(); + var middleware = SetUp(HttpStatusCode.BadRequest, "multipart/form-data;", httpContextMock, responseBody); + + await middleware.InvokeAsync(httpContextMock.Object); + + responseBody.Seek(0, SeekOrigin.Begin); + var reader = new StreamReader(responseBody); + var responseBodyText = await reader.ReadToEndAsync(); + Assert.Contains("The uploaded file is too large. Maximum allowed size is", responseBodyText); + } + + [Fact] + public async Task InvokeAsync_ShouldNotWriteResponse_WhenStatusCodeIsNot400() + { + var responseBody = new MemoryStream(); + var httpContextMock = new Mock(); + var middleware = SetUp(HttpStatusCode.OK, "multipart/form-data;", httpContextMock, responseBody); + + await middleware.InvokeAsync(httpContextMock.Object); + + responseBody.Seek(0, SeekOrigin.Begin); + var reader = new StreamReader(responseBody); + var responseBodyText = await reader.ReadToEndAsync(); + Assert.Contains(string.Empty, responseBodyText); + } + + [Fact] + public async Task InvokeAsync_ShouldNotWriteResponse_WhenContentTypeIsNotMultipartFormData() + { + var responseBody = new MemoryStream(); + var httpContextMock = new Mock(); + var middleware = SetUp(HttpStatusCode.BadRequest, "application/json", httpContextMock, responseBody); + + await middleware.InvokeAsync(httpContextMock.Object); + + responseBody.Seek(0, SeekOrigin.Begin); + var reader = new StreamReader(responseBody); + var responseBodyText = await reader.ReadToEndAsync(); + Assert.Contains(string.Empty, responseBodyText); + } + + private FileUploadMiddleware SetUp(HttpStatusCode statusCode, + string contentType, + Mock httpContextMock, + MemoryStream responseBody) + { + var requestMock = new Mock(); + var responseMock = new Mock(); + var requestDelegateMock = new Mock(); + + responseMock.Setup(r => r.StatusCode).Returns((int)statusCode); + requestMock.Setup(h => h.ContentType).Returns(contentType); + + httpContextMock.Setup(h => h.Request).Returns(requestMock.Object); + httpContextMock.Setup(h => h.Response).Returns(responseMock.Object); + responseMock.Setup(res => res.Body).Returns(responseBody); + + return new FileUploadMiddlewareTest(requestDelegateMock.Object); + } +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/FileUploadMiddleware.cs b/Frontend/CO.CDP.OrganisationApp/FileUploadMiddleware.cs new file mode 100644 index 000000000..043d85212 --- /dev/null +++ b/Frontend/CO.CDP.OrganisationApp/FileUploadMiddleware.cs @@ -0,0 +1,25 @@ +using CO.CDP.OrganisationApp.Pages.Forms; +using System.Net; + +namespace CO.CDP.OrganisationApp; + +public class FileUploadMiddleware(RequestDelegate next) +{ + public async Task InvokeAsync(HttpContext context) + { + await next(context); + + string contentType = context.Request.ContentType ?? string.Empty; + bool isMultipartFormData = contentType.Contains("multipart/form-data;"); + + if ((context.Response.StatusCode == (int)HttpStatusCode.BadRequest) && (isMultipartFormData)) + { + await WriteAsync(context); + } + } + + protected virtual async Task WriteAsync(HttpContext context) + { + await context.Response.WriteAsync($"The uploaded file is too large. Maximum allowed size is {FormElementFileUploadModel.AllowedMaxFileSizeMB} MB."); + } +} \ No newline at end of file diff --git a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormElementFileUploadModel.cs b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormElementFileUploadModel.cs index c562996d4..61611546d 100644 --- a/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormElementFileUploadModel.cs +++ b/Frontend/CO.CDP.OrganisationApp/Pages/Forms/FormElementFileUploadModel.cs @@ -9,7 +9,7 @@ namespace CO.CDP.OrganisationApp.Pages.Forms; public class FormElementFileUploadModel : FormElementModel, IValidatableObject { - private const int AllowedMaxFileSizeMB = 25; + public const int AllowedMaxFileSizeMB = 25; private readonly string[] AllowedExtensions = [".jpg", ".jpeg", ".png", ".pdf", ".txt", ".xls", ".xlsx", ".csv", ".docx", ".doc"]; [BindProperty] diff --git a/Frontend/CO.CDP.OrganisationApp/Program.cs b/Frontend/CO.CDP.OrganisationApp/Program.cs index 0d574eed1..cd49f277f 100644 --- a/Frontend/CO.CDP.OrganisationApp/Program.cs +++ b/Frontend/CO.CDP.OrganisationApp/Program.cs @@ -1,17 +1,21 @@ +using Amazon.SQS; using CO.CDP.AwsServices; +using CO.CDP.AwsServices.Sqs; using CO.CDP.Configuration.ForwardedHeaders; using CO.CDP.DataSharing.WebApiClient; using CO.CDP.EntityVerificationClient; using CO.CDP.Forms.WebApiClient; using CO.CDP.Localization; +using CO.CDP.MQ; using CO.CDP.Organisation.WebApiClient; using CO.CDP.OrganisationApp; using CO.CDP.OrganisationApp.Authentication; using CO.CDP.OrganisationApp.Authorization; using CO.CDP.OrganisationApp.Pages; +using CO.CDP.OrganisationApp.Pages.Forms; using CO.CDP.OrganisationApp.Pages.Forms.ChoiceProviderStrategies; -using CO.CDP.OrganisationApp.ThirdPartyApiClients.CompaniesHouse; using CO.CDP.OrganisationApp.ThirdPartyApiClients.CharityCommission; +using CO.CDP.OrganisationApp.ThirdPartyApiClients.CompaniesHouse; using CO.CDP.OrganisationApp.WebApiClients; using CO.CDP.Person.WebApiClient; using CO.CDP.Tenant.WebApiClient; @@ -19,18 +23,16 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.Options; +using Microsoft.FeatureManagement; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using System.Globalization; using static IdentityModel.OidcConstants; using static System.Net.Mime.MediaTypeNames; using ISession = CO.CDP.OrganisationApp.ISession; -using Microsoft.FeatureManagement; -using Amazon.SQS; -using CO.CDP.AwsServices.Sqs; -using CO.CDP.MQ; -using Microsoft.Extensions.Options; const string FormsHttpClientName = "FormsHttpClient"; const string TenantHttpClientName = "TenantHttpClient"; @@ -61,6 +63,11 @@ }; }); +builder.Services.Configure(options => +{ + options.MultipartBodyLengthLimit = (FormElementFileUploadModel.AllowedMaxFileSizeMB * 2) * 1024 * 1024; +}); + builder.Services.AddFeatureManagement(builder.Configuration.GetSection("Features")); var mvcBuilder = builder.Services.AddRazorPages() @@ -308,6 +315,8 @@ app.UseAuthentication(); app.UseSession(); app.UseMiddleware(); +app.UseMiddleware(); + app.UseAuthorization(); app.MapRazorPages();