From 6f85ca80d8d3c07796fa2eae40e6984d96665282 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Mon, 21 Oct 2024 09:50:30 +0200 Subject: [PATCH] Add tests for too big upload --- .../Controllers/DataControllerTests.cs | 444 ++++++++++-------- test/Altinn.App.Api.Tests/Data/TestData.cs | 31 +- .../Mocks/InstanceClientMockSi.cs | 16 +- 3 files changed, 269 insertions(+), 222 deletions(-) diff --git a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs index 86a82873e..6793a6e70 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs @@ -11,240 +11,282 @@ using Microsoft.Extensions.DependencyInjection; using Xunit.Abstractions; -namespace Altinn.App.Api.Tests.Controllers; - -public class DataControllerTests : ApiTestBase, IClassFixture> +namespace Altinn.App.Api.Tests.Controllers { - public DataControllerTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) - : base(factory, outputHelper) { } - - [Fact] - public async Task PutDataElement_MissingDataType_ReturnsBadRequest() + public class DataControllerTests : ApiTestBase, IClassFixture> { - // Setup test data - string org = "tdd"; - string app = "contributer-restriction"; - int instanceOwnerPartyId = 1337; - Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); - HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - - TestData.DeleteInstance(org, app, instanceOwnerPartyId, guid); - TestData.PrepareInstance(org, app, instanceOwnerPartyId, guid); - - using var content = new StringContent("{}", System.Text.Encoding.UTF8, "application/json"); // empty valid json - var response = await client.PostAsync($"/{org}/{app}/instances/{instanceOwnerPartyId}/{guid}/data", content); - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - var responseContent = await response.Content.ReadAsStringAsync(); - responseContent.Should().Contain("dataType"); - } + public DataControllerTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) + : base(factory, outputHelper) { } - [Fact] - public async Task CreateDataElement_BinaryPdf_AnalyserShouldRunOk() - { - OverrideServicesForThisTest = (services) => + [Fact] + public async Task PutDataElement_MissingDataType_ReturnsBadRequest() + { + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 1337; + Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + TestData.DeleteInstance(org, app, instanceOwnerPartyId, guid); + TestData.PrepareInstance(org, app, instanceOwnerPartyId, guid); + + using var content = new StringContent("{}", System.Text.Encoding.UTF8, "application/json"); // empty valid json + var response = await client.PostAsync( + $"/{org}/{app}/instances/{instanceOwnerPartyId}/{guid}/data", + content + ); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + var responseContent = await response.Content.ReadAsStringAsync(); + responseContent.Should().Contain("dataType"); + } + + [Fact] + public async Task PostBinaryElement_ContentTooLarge_ReturnsBadRequest() + { + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + int instanceOwnerPartyId = 1337; + string dataType = "specificFileType"; // Should have restrictions on 1 mb in app metadata + Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); + HttpClient client = GetRootedClient(org, app); + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + TestData.DeleteInstance(org, app, instanceOwnerPartyId, guid); + TestData.PrepareInstance(org, app, instanceOwnerPartyId, guid); + + using var content = new ByteArrayContent(new byte[1024 * 1024 + 1]); // 1 mb + content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); + content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") + { + FileName = "example.pdf" + }; + var response = await client.PostAsync( + $"/{org}/{app}/instances/{instanceOwnerPartyId}/{guid}/data/{dataType}", + content + ); + var responseContent = await response.Content.ReadAsStringAsync(); + OutputHelper.WriteLine(responseContent); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + responseContent + .Should() + .Contain( + """ + "code":"DataElementTooLarge","description":"Invalid data provided. Error: Binary attachment exceeds limit of 1048576","source":"DataRestrictionValidation" + """ + ); + } + + [Fact] + public async Task CreateDataElement_BinaryPdf_AnalyserShouldRunOk() { - services.AddTransient(); - services.AddTransient(); - }; + OverrideServicesForThisTest = (services) => + { + services.AddTransient(); + services.AddTransient(); + }; - // Setup test data - string org = "tdd"; - string app = "contributer-restriction"; - HttpClient client = GetRootedClient(org, app); + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); - Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); - TestData.DeleteInstance(org, app, 1337, guid); - TestData.PrepareInstance(org, app, 1337, guid); + Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); - // Setup the request - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - ByteArrayContent fileContent = await CreateBinaryContent(org, app, "example.pdf", "application/pdf"); - string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; - var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = fileContent }; + // Setup the request + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + ByteArrayContent fileContent = await CreateBinaryContent(org, app, "example.pdf", "application/pdf"); + string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; + var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = fileContent }; - // This is where it happens - HttpResponseMessage response = await client.SendAsync(request); + // This is where it happens + HttpResponseMessage response = await client.SendAsync(request); - // Cleanup testdata - TestData.DeleteInstanceAndData(org, app, 1337, guid); + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - } + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } - [Fact] - public async Task CreateDataElement_ZeroBytes_BinaryPdf_AnalyserShouldReturnBadRequest() - { - OverrideServicesForThisTest = (services) => + [Fact] + public async Task CreateDataElement_ZeroBytes_BinaryPdf_AnalyserShouldReturnBadRequest() { - services.AddTransient(); - services.AddTransient(); - }; - - // Setup test data - string org = "tdd"; - string app = "contributer-restriction"; - HttpClient client = GetRootedClient(org, app); - - Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); - TestData.DeleteInstance(org, app, 1337, guid); - TestData.PrepareInstance(org, app, 1337, guid); - - // Setup the request - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - ByteArrayContent fileContent = await CreateBinaryContent(org, app, "zero.pdf", "application/pdf"); - string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; - var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = fileContent }; - - // This is where it happens - HttpResponseMessage response = await client.SendAsync(request); - - // Cleanup testdata - TestData.DeleteInstanceAndData(org, app, 1337, guid); - - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - var responseContent = await response.Content.ReadAsStringAsync(); - Assert.Equal("Invalid data provided. Error: The file is zero bytes.", responseContent); - } + OverrideServicesForThisTest = (services) => + { + services.AddTransient(); + services.AddTransient(); + }; + + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + + Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + + // Setup the request + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + ByteArrayContent fileContent = await CreateBinaryContent(org, app, "zero.pdf", "application/pdf"); + string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; + var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = fileContent }; + + // This is where it happens + HttpResponseMessage response = await client.SendAsync(request); + + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var responseContent = await response.Content.ReadAsStringAsync(); + Assert.Equal("Invalid data provided. Error: The file is zero bytes.", responseContent); + } - [Fact] - public async Task CreateDataElement_JpgFakedAsPdf_AnalyserShouldRunAndFail() - { - OverrideServicesForThisTest = (services) => + [Fact] + public async Task CreateDataElement_JpgFakedAsPdf_AnalyserShouldRunAndFail() + { + OverrideServicesForThisTest = (services) => + { + services.AddTransient(); + services.AddTransient(); + }; + + // Setup test data + string org = "tdd"; + string app = "contributer-restriction"; + HttpClient client = GetRootedClient(org, app); + + Guid guid = new Guid("1fc98a23-fe31-4ef5-8fb9-dd3f479354ce"); + TestData.DeleteInstance(org, app, 1337, guid); + TestData.PrepareInstance(org, app, 1337, guid); + + // Setup the request + string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + ByteArrayContent fileContent = await CreateBinaryContent(org, app, "example.jpg.pdf", "application/pdf"); + string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; + var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = fileContent }; + + // This is where it happens + HttpResponseMessage response = await client.SendAsync(request); + string responseContent = await response.Content.ReadAsStringAsync(); + + // Cleanup testdata + TestData.DeleteInstanceAndData(org, app, 1337, guid); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + private static async Task CreateBinaryContent( + string org, + string app, + string filename, + string mediaType + ) { - services.AddTransient(); - services.AddTransient(); - }; - - // Setup test data - string org = "tdd"; - string app = "contributer-restriction"; - HttpClient client = GetRootedClient(org, app); - - Guid guid = new Guid("1fc98a23-fe31-4ef5-8fb9-dd3f479354ce"); - TestData.DeleteInstance(org, app, 1337, guid); - TestData.PrepareInstance(org, app, 1337, guid); - - // Setup the request - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - ByteArrayContent fileContent = await CreateBinaryContent(org, app, "example.jpg.pdf", "application/pdf"); - string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; - var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = fileContent }; - - // This is where it happens - HttpResponseMessage response = await client.SendAsync(request); - string responseContent = await response.Content.ReadAsStringAsync(); - - // Cleanup testdata - TestData.DeleteInstanceAndData(org, app, 1337, guid); - - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var pdfFilePath = TestData.GetAppSpecificTestdataFile(org, app, filename); + var fileBytes = await File.ReadAllBytesAsync(pdfFilePath); + var fileContent = new ByteArrayContent(fileBytes); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(mediaType); + fileContent.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse( + $"attachment; filename=\"{filename}\"; filename*=UTF-8''{filename}" + ); + return fileContent; + } } - private static async Task CreateBinaryContent( - string org, - string app, - string filename, - string mediaType - ) + public class MimeTypeAnalyserSuccessStub : IFileAnalyser { - var pdfFilePath = TestData.GetAppSpecificTestdataFile(org, app, filename); - var fileBytes = await File.ReadAllBytesAsync(pdfFilePath); - var fileContent = new ByteArrayContent(fileBytes); - fileContent.Headers.ContentType = new MediaTypeHeaderValue(mediaType); - fileContent.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse( - $"attachment; filename=\"{filename}\"; filename*=UTF-8''{filename}" - ); - return fileContent; - } -} + public string Id { get; private set; } = "mimeTypeAnalyser"; -public class MimeTypeAnalyserSuccessStub : IFileAnalyser -{ - public string Id { get; private set; } = "mimeTypeAnalyser"; + public Task> Analyse(IEnumerable httpContents) + { + throw new NotImplementedException(); + } - public Task> Analyse(IEnumerable httpContents) - { - throw new NotImplementedException(); + public Task Analyse(Stream stream, string? filename = null) + { + return Task.FromResult( + new FileAnalysisResult(Id) + { + MimeType = "application/pdf", + Filename = "example.pdf", + Extensions = new List() { "pdf" } + } + ); + } } - public Task Analyse(Stream stream, string? filename = null) + public class MimeTypeAnalyserFailureStub : IFileAnalyser { - return Task.FromResult( - new FileAnalysisResult(Id) - { - MimeType = "application/pdf", - Filename = "example.pdf", - Extensions = new List() { "pdf" } - } - ); - } -} + public string Id { get; private set; } = "mimeTypeAnalyser"; -public class MimeTypeAnalyserFailureStub : IFileAnalyser -{ - public string Id { get; private set; } = "mimeTypeAnalyser"; + public Task> Analyse(IEnumerable httpContents) + { + throw new NotImplementedException(); + } - public Task> Analyse(IEnumerable httpContents) - { - throw new NotImplementedException(); + public Task Analyse(Stream stream, string? filename = null) + { + return Task.FromResult( + new FileAnalysisResult(Id) + { + MimeType = "application/jpeg", + Filename = "example.jpg.pdf", + Extensions = new List() { "jpg" } + } + ); + } } - public Task Analyse(Stream stream, string? filename = null) + public class MimeTypeValidatorStub : IFileValidator { - return Task.FromResult( - new FileAnalysisResult(Id) - { - MimeType = "application/jpeg", - Filename = "example.jpg.pdf", - Extensions = new List() { "jpg" } - } - ); - } -} - -public class MimeTypeValidatorStub : IFileValidator -{ - public string Id { get; private set; } = "mimeTypeValidator"; + public string Id { get; private set; } = "mimeTypeValidator"; #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously. Suppressed because of the interface. - public async Task<(bool Success, IEnumerable Errors)> Validate( - DataType dataType, - IEnumerable fileAnalysisResults - ) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - { - List errors = new(); - - var fileMimeTypeResult = fileAnalysisResults.FirstOrDefault(x => x.MimeType != null); - - // Verify that file mime type is an allowed content-type - if ( - !dataType.AllowedContentTypes.Contains( - fileMimeTypeResult?.MimeType, - StringComparer.InvariantCultureIgnoreCase - ) && !dataType.AllowedContentTypes.Contains("application/octet-stream") + public async Task<(bool Success, IEnumerable Errors)> Validate( + DataType dataType, + IEnumerable fileAnalysisResults ) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { - ValidationIssue error = - new() - { - Source = ValidationIssueSources.File, - Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, - Severity = ValidationIssueSeverity.Error, - Description = - $"The {fileMimeTypeResult?.Filename + " "}file does not appear to be of the allowed content type according to the configuration for data type {dataType.Id}. Allowed content types are {string.Join(", ", dataType.AllowedContentTypes)}" - }; + List errors = new(); - errors.Add(error); + var fileMimeTypeResult = fileAnalysisResults.FirstOrDefault(x => x.MimeType != null); - return (false, errors); - } + // Verify that file mime type is an allowed content-type + if ( + !dataType.AllowedContentTypes.Contains( + fileMimeTypeResult?.MimeType, + StringComparer.InvariantCultureIgnoreCase + ) && !dataType.AllowedContentTypes.Contains("application/octet-stream") + ) + { + ValidationIssue error = + new() + { + Source = ValidationIssueSources.File, + Code = ValidationIssueCodes.DataElementCodes.ContentTypeNotAllowed, + Severity = ValidationIssueSeverity.Error, + Description = + $"The {fileMimeTypeResult?.Filename + " "}file does not appear to be of the allowed content type according to the configuration for data type {dataType.Id}. Allowed content types are {string.Join(", ", dataType.AllowedContentTypes)}" + }; + + errors.Add(error); + + return (false, errors); + } - return (true, errors); + return (true, errors); + } } } diff --git a/test/Altinn.App.Api.Tests/Data/TestData.cs b/test/Altinn.App.Api.Tests/Data/TestData.cs index ba491d121..2fe0a27ce 100644 --- a/test/Altinn.App.Api.Tests/Data/TestData.cs +++ b/test/Altinn.App.Api.Tests/Data/TestData.cs @@ -47,27 +47,27 @@ public static string GetApplicationMetadataPath(string org, string app) return Path.Combine(applicationMetadataPath, "config", "applicationmetadata.json"); } - public static string GetInstancesDirectory() + public static string GetInstancesDirectory(string org, string app) { string? testDataDirectory = GetTestDataRootDirectory(); - return Path.Combine(testDataDirectory!, @"Instances"); + return Path.Combine(testDataDirectory!, @"Instances", org, app); } public static string GetDataDirectory(string org, string app, int instanceOwnerId, Guid instanceGuid) { - string instancesDirectory = GetInstancesDirectory(); + string instancesDirectory = GetInstancesDirectory(org, app); - return Path.Join(instancesDirectory, org, app, instanceOwnerId.ToString(), instanceGuid.ToString()) + return Path.Join(instancesDirectory, instanceOwnerId.ToString(), instanceGuid.ToString()) + Path.DirectorySeparatorChar; } public static (string org, string app) GetInstanceOrgApp(InstanceIdentifier identifier) { - string instancesDirectory = GetInstancesDirectory(); + string instancesDirectory = GetTestDataRootDirectory(); var instanceOwner = identifier.InstanceOwnerPartyId.ToString(); var instanceId = identifier.InstanceGuid.ToString(); - foreach (var org in Directory.GetDirectories(instancesDirectory)) + foreach (var org in Directory.GetDirectories(Path.Join(instancesDirectory, "Instances"))) { foreach (var app in Directory.GetDirectories(org)) { @@ -136,17 +136,30 @@ public static string GetRegisterProfilePath() public static void DeleteInstance(string org, string app, int instanceOwnerId, Guid instanceGuid) { - string instancePath = GetInstancePath(org, app, instanceOwnerId, instanceGuid); + // Delete the instance document + var instancesDirectory = GetInstancesDirectory(org, app); + var instancePath = Path.Join(instancesDirectory, instanceOwnerId.ToString(), instanceGuid + @".json"); if (File.Exists(instancePath)) { File.Delete(instancePath); } + // Empty the data folder, but keep the .pretest files + var instanceDataFolder = Path.Join(instancesDirectory, instanceOwnerId.ToString(), instanceGuid.ToString()); + + var filesToDelete = Directory + .GetFiles(instanceDataFolder, "*", SearchOption.AllDirectories) + .Where(file => !file.EndsWith(".pretest.json") && !file.EndsWith(".pretest")); + + foreach (var file in filesToDelete) + { + File.Delete(file); + } } public static string GetInstancePath(string org, string app, int instanceOwnerId, Guid instanceGuid) { - string instancesDirectory = GetInstancesDirectory(); - return Path.Combine(instancesDirectory, org, app, instanceOwnerId.ToString(), instanceGuid + @".json"); + string instancesDirectory = GetInstancesDirectory(org, app); + return Path.Combine(instancesDirectory, instanceOwnerId.ToString(), instanceGuid + @".json"); } public static void PrepareInstance(string org, string app, int instanceOwnerId, Guid instanceGuid) diff --git a/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs index 4c2dc556d..cc169d50a 100644 --- a/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs +++ b/test/Altinn.App.Api.Tests/Mocks/InstanceClientMockSi.cs @@ -150,7 +150,7 @@ private static Instance GetTestInstance(string app, string org, int instanceOwne private static string GetInstancePath(int instanceOwnerPartyId, Guid instanceGuid) { string[] paths = Directory.GetFiles( - TestData.GetInstancesDirectory(), + Path.Join(TestData.GetTestDataRootDirectory(), "Instances"), instanceGuid + ".json", SearchOption.AllDirectories ); @@ -165,21 +165,13 @@ private static string GetInstancePath(int instanceOwnerPartyId, Guid instanceGui private static string GetInstancePath(string app, string org, int instanceOwnerId, Guid instanceId) { - return Path.Combine( - TestData.GetInstancesDirectory(), - org, - app, - instanceOwnerId.ToString(), - instanceId + ".json" - ); + return Path.Combine(TestData.GetInstancesDirectory(org, app), instanceOwnerId.ToString(), instanceId + ".json"); } private static string GetDataPath(string org, string app, int instanceOwnerId, Guid instanceGuid) { return Path.Combine( - TestData.GetInstancesDirectory(), - org, - app, + TestData.GetInstancesDirectory(org, app), instanceOwnerId.ToString(), instanceGuid.ToString() ) + Path.DirectorySeparatorChar; @@ -480,7 +472,7 @@ public async Task> GetInstances(Dictionary List instances = new(); - string instancesPath = TestData.GetInstancesDirectory(); + string instancesPath = Path.Join(TestData.GetTestDataRootDirectory(), "Instances"); int fileDepth = 4;