From 37144a03f7f7bd4538b41a05d2ab85b1a7a4907a Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 19 Jul 2025 11:01:54 +0200 Subject: [PATCH] Adds missing test cases of temporary files --- .../multipart/JakartaMultiPartRequest.java | 14 +- .../JakartaStreamMultiPartRequest.java | 14 -- .../AbstractMultiPartRequestTest.java | 157 ++++++++++++ .../JakartaMultiPartRequestTest.java | 172 +++++++++++++ .../JakartaStreamMultiPartRequestTest.java | 230 ++++++++++++++++++ 5 files changed, 568 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java index 23d93d585..0b4ce88fe 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java @@ -209,7 +209,7 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest { List values = uploadedFiles.computeIfAbsent(fieldName, k -> new ArrayList<>()); if (item.isInMemory()) { - LOG.debug("Creating temporary file representing in-memory uploaded item: {}", normalizeSpace(item.getFieldName())); + LOG.debug(() -> "Creating temporary file representing in-memory uploaded item: " + normalizeSpace(item.getFieldName())); try { File tempFile = createTemporaryFile(item.getName(), Path.of(saveDir)); @@ -229,8 +229,10 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest { .build(); values.add(uploadedFile); - LOG.debug("Created temporary file for in-memory uploaded item: {} at {}", - normalizeSpace(item.getName()), tempFile.getAbsolutePath()); + if (LOG.isDebugEnabled()) { + LOG.debug("Created temporary file for in-memory uploaded item: {} at {}", + normalizeSpace(item.getName()), tempFile.getAbsolutePath()); + } } catch (IOException e) { LOG.warn("Failed to create temporary file for in-memory uploaded item: {}", normalizeSpace(item.getName()), e); @@ -277,10 +279,12 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest { for (DiskFileItem item : diskFileItems) { try { if (item.isInMemory()) { - LOG.debug("Cleaning up in-memory item: {}", normalizeSpace(item.getFieldName())); + LOG.debug(() -> "Cleaning up in-memory item: " + normalizeSpace(item.getFieldName())); } else { Path itemPath = item.getPath(); - LOG.debug("Cleaning up disk item: {} at {}", normalizeSpace(item.getFieldName()), itemPath); + if (LOG.isDebugEnabled()) { + LOG.debug("Cleaning up disk item: {} at {}", normalizeSpace(item.getFieldName()), itemPath); + } if (itemPath != null) { File itemFile = itemPath.toFile(); if (itemFile.exists() && !itemFile.delete()) { diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java index 9e59aa766..7275743e5 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java @@ -242,20 +242,6 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { } } - /** - * Creates a temporary file based on the given filename and location. - * - * @param fileName file name - * @param location location - * @return a temporary file based on the given filename and location - */ - protected File createTemporaryFile(String fileName, Path location) { - String uid = UUID.randomUUID().toString().replace("-", "_"); - File file = location.resolve("upload_" + uid + ".tmp").toFile(); - LOG.debug("Creating temporary file: {} (originally: {})", file.getName(), fileName); - return file; - } - /** * Streams the file upload stream to the specified file. * diff --git a/core/src/test/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequestTest.java b/core/src/test/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequestTest.java index d968ec256..98d1325ef 100644 --- a/core/src/test/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequestTest.java +++ b/core/src/test/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequestTest.java @@ -30,6 +30,9 @@ import org.springframework.mock.web.MockHttpServletRequest; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -586,6 +589,160 @@ abstract class AbstractMultiPartRequestTest { } } + @Test + public void createTemporaryFileGeneratesSecureNames() { + // Create a test instance to access the protected method + AbstractMultiPartRequest testRequest = createMultipartRequest(); + Path testLocation = Paths.get(tempDir); + + // when - create multiple temporary files + File tempFile1 = testRequest.createTemporaryFile("test1.csv", testLocation); + File tempFile2 = testRequest.createTemporaryFile("test2.csv", testLocation); + File tempFile3 = testRequest.createTemporaryFile("../../../malicious.csv", testLocation); + + // then - verify secure naming + assertThat(tempFile1.getName()).startsWith("upload_"); + assertThat(tempFile1.getName()).endsWith(".tmp"); + assertThat(tempFile2.getName()).startsWith("upload_"); + assertThat(tempFile2.getName()).endsWith(".tmp"); + assertThat(tempFile3.getName()).startsWith("upload_"); + assertThat(tempFile3.getName()).endsWith(".tmp"); + + // Verify each file has a unique name + assertThat(tempFile1.getName()).isNotEqualTo(tempFile2.getName()); + assertThat(tempFile2.getName()).isNotEqualTo(tempFile3.getName()); + assertThat(tempFile1.getName()).isNotEqualTo(tempFile3.getName()); + + // Verify all files are in the correct location + assertThat(tempFile1.getParent()).isEqualTo(tempDir); + assertThat(tempFile2.getParent()).isEqualTo(tempDir); + assertThat(tempFile3.getParent()).isEqualTo(tempDir); + + // Verify malicious filename doesn't affect the location + assertThat(tempFile3.getName()).doesNotContain(".."); + assertThat(tempFile3.getName()).doesNotContain("/"); + assertThat(tempFile3.getName()).doesNotContain("\\"); + + // Clean up test files + tempFile1.delete(); + tempFile2.delete(); + tempFile3.delete(); + } + + @Test + public void createTemporaryFileInSpecificDirectory() throws IOException { + // Create a subdirectory for testing + Path subDir = Paths.get(tempDir, "subdir"); + Files.createDirectories(subDir); + + AbstractMultiPartRequest testRequest = createMultipartRequest(); + + // when + File tempFile = testRequest.createTemporaryFile("test.csv", subDir); + + // then - verify file is created in the specified subdirectory + assertThat(tempFile.getParent()).isEqualTo(subDir.toString()); + assertThat(tempFile.getName()).startsWith("upload_"); + assertThat(tempFile.getName()).endsWith(".tmp"); + + // Clean up + tempFile.delete(); + Files.delete(subDir); + } + + @Test + public void createTemporaryFileWithNullFileName() throws IOException { + AbstractMultiPartRequest testRequest = createMultipartRequest(); + Path testLocation = Paths.get(tempDir); + + // when - create temp file with null filename + File tempFile = testRequest.createTemporaryFile(null, testLocation); + + // then - should still create a valid temporary file + assertThat(tempFile.getName()).startsWith("upload_"); + assertThat(tempFile.getName()).endsWith(".tmp"); + assertThat(tempFile.getParent()).isEqualTo(tempDir); + + // Clean up + tempFile.delete(); + } + + @Test + public void createTemporaryFileWithEmptyFileName() throws IOException { + AbstractMultiPartRequest testRequest = createMultipartRequest(); + Path testLocation = Paths.get(tempDir); + + // when - create temp file with empty filename + File tempFile = testRequest.createTemporaryFile("", testLocation); + + // then - should still create a valid temporary file + assertThat(tempFile.getName()).startsWith("upload_"); + assertThat(tempFile.getName()).endsWith(".tmp"); + assertThat(tempFile.getParent()).isEqualTo(tempDir); + + // Clean up + tempFile.delete(); + } + + @Test + public void createTemporaryFileWithSpecialCharacters() { + AbstractMultiPartRequest testRequest = createMultipartRequest(); + Path testLocation = Paths.get(tempDir); + + // when - create temp files with various special characters + File tempFile1 = testRequest.createTemporaryFile("file with spaces.csv", testLocation); + File tempFile2 = testRequest.createTemporaryFile("file@#$%^&*().csv", testLocation); + File tempFile3 = testRequest.createTemporaryFile("файл.csv", testLocation); // Cyrillic + + // then - all should create valid secure temporary files + File[] tempFiles = {tempFile1, tempFile2, tempFile3}; + for (File tempFile : tempFiles) { + assertThat(tempFile.getName()).startsWith("upload_"); + assertThat(tempFile.getName()).endsWith(".tmp"); + assertThat(tempFile.getParent()).isEqualTo(tempDir); + // Verify no special characters leak into the actual filename + assertThat(tempFile.getName()).matches("upload_[a-zA-Z0-9_]+\\.tmp"); + } + + // All should have unique names + assertThat(tempFile1.getName()).isNotEqualTo(tempFile2.getName()); + assertThat(tempFile2.getName()).isNotEqualTo(tempFile3.getName()); + assertThat(tempFile1.getName()).isNotEqualTo(tempFile3.getName()); + + // Clean up + tempFile1.delete(); + tempFile2.delete(); + tempFile3.delete(); + } + + @Test + public void createTemporaryFileConsistentNaming() { + AbstractMultiPartRequest testRequest = createMultipartRequest(); + Path testLocation = Paths.get(tempDir); + + // when - create many temporary files to verify naming consistency + List tempFiles = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + tempFiles.add(testRequest.createTemporaryFile("test" + i + ".csv", testLocation)); + } + + // then - all should follow the same naming pattern + for (File tempFile : tempFiles) { + assertThat(tempFile.getName()).startsWith("upload_"); + assertThat(tempFile.getName()).endsWith(".tmp"); + assertThat(tempFile.getParent()).isEqualTo(tempDir); + // Verify UUID pattern (without hyphens, replaced with underscores) + assertThat(tempFile.getName()).matches("upload_[a-zA-Z0-9_]+\\.tmp"); + } + + // Verify all names are unique + List fileNames = tempFiles.stream().map(File::getName).toList(); + assertThat(fileNames).doesNotHaveDuplicates(); + + // Clean up + tempFiles.forEach(File::delete); + } + protected String formFile(String fieldName, String filename, String content) { return endline + "--" + boundary + endline + diff --git a/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java b/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java index 2ef0782ea..15b59f5dd 100644 --- a/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java +++ b/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java @@ -20,6 +20,7 @@ package org.apache.struts2.dispatcher.multipart; import org.apache.commons.fileupload2.core.DiskFileItem; import org.apache.struts2.dispatcher.LocalizedMessage; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.Test; import java.io.File; @@ -281,4 +282,175 @@ public class JakartaMultiPartRequestTest extends AbstractMultiPartRequestTest { } } + @Test + public void processNormalFormFieldHandlesNullFieldName() throws IOException { + // Test null field name handling in processNormalFormField + String content = + endline + "--" + boundary + endline + + "Content-Disposition: form-data" + endline + // No name attribute + endline + + "field value without name" + + endline + "--" + boundary + endline + + "Content-Disposition: form-data; name=\"validfield\"" + endline + + endline + + "valid field value" + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when + multiPart.parse(mockRequest, tempDir); + + // then - should only process the valid field + assertThat(multiPart.getErrors()).isEmpty(); + assertThat(multiPart.getParameter("validfield")).isEqualTo("valid field value"); + assertThat(multiPart.getParameterNames().asIterator()).toIterable().hasSize(1); + } + + @Test + public void processFileFieldHandlesNullFieldName() throws IOException { + // Test null field name handling in processFileField + String content = + endline + "--" + boundary + endline + + "Content-Disposition: form-data; filename=\"orphan.txt\"" + endline + // No name attribute + "Content-Type: text/plain" + endline + + endline + + "orphaned file content" + + endline + "--" + boundary + endline + + "Content-Disposition: form-data; name=\"validfile\"; filename=\"valid.txt\"" + endline + + "Content-Type: text/plain" + endline + + endline + + "valid file content" + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when + multiPart.parse(mockRequest, tempDir); + + // then - should only process the valid file + assertThat(multiPart.getErrors()).isEmpty(); + assertThat(multiPart.uploadedFiles).hasSize(1); + assertThat(multiPart.getFile("validfile")).hasSize(1); + assertThat(multiPart.getFile("validfile")[0].getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .content() + .isEqualTo("valid file content"); + } + + @Test + public void diskFileItemCleanupCoverage() throws IOException, NoSuchFieldException, IllegalAccessException { + // Test disk file item cleanup paths + String content = formFile("file1", "test1.csv", "1,2,3,4") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when - force files to disk with small buffer + multiPart.setBufferSize("1"); + multiPart.parse(mockRequest, tempDir); + + // Access private field to verify disk file items are tracked + Field diskFileItemsField = JakartaMultiPartRequest.class.getDeclaredField("diskFileItems"); + diskFileItemsField.setAccessible(true); + @SuppressWarnings("unchecked") + java.util.List diskFileItems = + (java.util.List) diskFileItemsField.get(multiPart); + + // then - should have disk file items tracked + assertThat(diskFileItems).isNotEmpty(); + + // when - cleanup + multiPart.cleanUp(); + + // then - should clear tracking + assertThat(diskFileItems).isEmpty(); + } + + @Test + public void inMemoryVsDiskFileHandling() throws IOException { + // Test both in-memory and disk file handling paths + String smallContent = "small"; // Should be in-memory + String largeContent = "x".repeat(20000); // Should go to disk + + String content = formFile("smallfile", "small.txt", smallContent) + + formFile("largefile", "large.txt", largeContent) + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when - use default buffer size + multiPart.parse(mockRequest, tempDir); + + // then - both files should be processed + assertThat(multiPart.getErrors()).isEmpty(); + assertThat(multiPart.uploadedFiles).hasSize(2); + assertThat(multiPart.getFile("smallfile")).hasSize(1); + assertThat(multiPart.getFile("largefile")).hasSize(1); + + // Verify content + assertThat(multiPart.getFile("smallfile")[0].getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .content() + .isEqualTo(smallContent); + assertThat(multiPart.getFile("largefile")[0].getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .content() + .isEqualTo(largeContent); + } + + @Test + public void errorDuplicationPrevention() throws IOException { + // Test that duplicate errors are not added + JakartaMultiPartRequest multiPartRequest = new JakartaMultiPartRequest(); + + // Simulate adding the same error multiple times + IOException testException = new IOException("Test error"); + LocalizedMessage errorMessage = multiPartRequest.buildErrorMessage( + testException.getClass(), testException.getMessage(), new Object[]{"test.csv"}); + + // when - try to add same error multiple times + multiPartRequest.errors.add(errorMessage); + if (!multiPartRequest.errors.contains(errorMessage)) { + multiPartRequest.errors.add(errorMessage); // Should not be added + } + if (!multiPartRequest.errors.contains(errorMessage)) { + multiPartRequest.errors.add(errorMessage); // Should not be added + } + + // then - should only have one error + assertThat(multiPartRequest.getErrors()).hasSize(1); + } + + @Test + public void processFileFieldHandlesEmptyFileName() throws IOException { + String content = + endline + "--" + boundary + endline + + "Content-Disposition: form-data; name=\"emptyfile\"; filename=\"\"" + endline + + "Content-Type: text/plain" + endline + + endline + + "some content that should be ignored" + + endline + "--" + boundary + endline + + "Content-Disposition: form-data; name=\"validfile\"; filename=\"test.txt\"" + endline + + "Content-Type: text/plain" + endline + + endline + + "valid file content" + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when + multiPart.parse(mockRequest, tempDir); + + // then - should only process the file with valid filename + assertThat(multiPart.getErrors()).isEmpty(); + assertThat(multiPart.uploadedFiles).hasSize(1); + assertThat(multiPart.getFile("validfile")).hasSize(1); + assertThat(multiPart.getFile("emptyfile")).isEmpty(); + assertThat(multiPart.getFile("validfile")[0].getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .content() + .isEqualTo("valid file content"); + } + } diff --git a/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequestTest.java b/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequestTest.java index a82c4b899..fc78021f5 100644 --- a/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequestTest.java +++ b/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequestTest.java @@ -23,11 +23,15 @@ import org.apache.struts2.dispatcher.LocalizedMessage; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.Test; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -161,4 +165,230 @@ public class JakartaStreamMultiPartRequestTest extends AbstractMultiPartRequestT assertThat(result.length()).isGreaterThan(1024); // Verify it's larger than internal buffer } + @Test + public void processFileItemAsFormFieldHandlesNullFieldName() throws IOException { + // Test the null field name path in processFileItemAsFormField + String content = formFile("", "test.csv", "data") + // Field name will be empty/null-like + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when + multiPart.parse(mockRequest, tempDir); + + // then - should complete without error, but no parameters should be added + assertThat(multiPart.getErrors()).isEmpty(); + } + + @Test + public void processFileItemAsFileFieldHandlesNullFieldName() throws IOException { + // This test covers the null field name path in processFileItemAsFileField + JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest(); + + // Create a mock file item with null field name + String content = "--" + boundary + endline + + "Content-Disposition: form-data; filename=\"test.csv\"" + endline + + "Content-Type: text/csv" + endline + + endline + + "test data" + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when + streamMultiPart.parse(mockRequest, tempDir); + + // then - should complete without error but no files should be uploaded + assertThat(streamMultiPart.getErrors()).isEmpty(); + assertThat(streamMultiPart.uploadedFiles).isEmpty(); + } + + @Test + public void exceedsMaxFilesPath() throws IOException { + // Test the exceedsMaxFiles method path + String content = formFile("file1", "test1.csv", "data1") + + formFile("file2", "test2.csv", "data2") + + formFile("file3", "test3.csv", "data3") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when - set max files to 1 + multiPart.setMaxFiles("1"); + multiPart.parse(mockRequest, tempDir); + + // then - should have only 1 file and errors for others + assertThat(multiPart.uploadedFiles).hasSize(1); + assertThat(multiPart.getErrors()) + .isNotEmpty() + .allSatisfy(error -> + assertThat(error.getTextKey()).isEqualTo("struts.messages.upload.error.FileUploadFileCountLimitException") + ); + } + + @Test + public void actualSizeOfUploadedFilesCalculation() throws IOException { + // Test the actualSizeOfUploadedFiles method + String content = formFile("file1", "test1.csv", "data1234567890") + // 14 bytes + headers + formFile("file2", "test2.csv", "moredata") + // 8 bytes + headers + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when + multiPart.parse(mockRequest, tempDir); + + // then - should have uploaded files and calculate their total size + assertThat(multiPart.uploadedFiles).hasSize(2); + assertThat(multiPart.getFile("file1")).hasSize(1); + assertThat(multiPart.getFile("file2")).hasSize(1); + + // Verify files have the expected content + assertThat(multiPart.getFile("file1")[0].getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .content() + .isEqualTo("data1234567890"); + assertThat(multiPart.getFile("file2")[0].getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .content() + .isEqualTo("moredata"); + } + + @Test + public void createTemporaryFileMethod() throws Exception { + // Test the createTemporaryFile method directly + JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest(); + Path testLocation = Paths.get(tempDir); + + // when + File tempFile1 = streamMultiPart.createTemporaryFile("test.csv", testLocation); + File tempFile2 = streamMultiPart.createTemporaryFile("another.txt", testLocation); + + // then + assertThat(tempFile1.getName()).startsWith("upload_"); + assertThat(tempFile1.getName()).endsWith(".tmp"); + assertThat(tempFile1.getParent()).isEqualTo(tempDir); + + assertThat(tempFile2.getName()).startsWith("upload_"); + assertThat(tempFile2.getName()).endsWith(".tmp"); + assertThat(tempFile2.getParent()).isEqualTo(tempDir); + + // Should be unique names + assertThat(tempFile1.getName()).isNotEqualTo(tempFile2.getName()); + + // Clean up + tempFile1.delete(); + tempFile2.delete(); + } + + @Test + public void streamFileToDiskWithDifferentBufferSizes() throws IOException { + // Test streamFileToDisk with different buffer sizes + String largeContent = "x".repeat(5000); // Content larger than default buffer + String content = formFile("largefile", "large.csv", largeContent) + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when - use small buffer size to ensure multiple reads + multiPart.setBufferSize("100"); + multiPart.parse(mockRequest, tempDir); + + // then + assertThat(multiPart.getErrors()).isEmpty(); + assertThat(multiPart.getFile("largefile")).hasSize(1); + assertThat(multiPart.getFile("largefile")[0].getContent()) + .asInstanceOf(InstanceOfAssertFactories.FILE) + .content() + .isEqualTo(largeContent); + } + + @Test + public void exceedsMaxSizeOfFilesWithFileCleanup() throws IOException { + // Test the file deletion path when max size is exceeded + String content = formFile("file1", "test1.csv", "small") + + formFile("file2", "test2.csv", "this is a much larger file content that should exceed the limit") + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when - set very small max size + multiPart.setMaxSizeOfFiles("20"); + multiPart.parse(mockRequest, tempDir); + + // then - should have first file uploaded but error for second + assertThat(multiPart.uploadedFiles).hasSize(1); + assertThat(multiPart.getFile("file1")).hasSize(1); + assertThat(multiPart.getFile("file2")).isEmpty(); + assertThat(multiPart.getErrors()) + .isNotEmpty() + .anyMatch(error -> + error.getTextKey().equals("struts.messages.upload.error.FileUploadSizeException") + ); + } + + @Test + public void createUploadedFileWithVariousContentTypes() throws IOException { + // Test different content types and file names + String content = + endline + "--" + boundary + endline + + "Content-Disposition: form-data; name=\"textfile\"; filename=\"document.txt\"" + endline + + "Content-Type: text/plain" + endline + + endline + + "Plain text content" + + endline + "--" + boundary + endline + + "Content-Disposition: form-data; name=\"jsonfile\"; filename=\"data.json\"" + endline + + "Content-Type: application/json" + endline + + endline + + "{\"key\": \"value\"}" + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when + multiPart.parse(mockRequest, tempDir); + + // then + assertThat(multiPart.getErrors()).isEmpty(); + assertThat(multiPart.uploadedFiles).hasSize(2); + + // Verify text file + assertThat(multiPart.getFile("textfile")).hasSize(1); + assertThat(multiPart.getFile("textfile")[0].getContentType()).isEqualTo("text/plain"); + assertThat(multiPart.getFile("textfile")[0].getOriginalName()).isEqualTo("document.txt"); + + // Verify JSON file + assertThat(multiPart.getFile("jsonfile")).hasSize(1); + assertThat(multiPart.getFile("jsonfile")[0].getContentType()).isEqualTo("application/json"); + assertThat(multiPart.getFile("jsonfile")[0].getOriginalName()).isEqualTo("data.json"); + } + + @Test + public void emptyFileNameFieldsAreSkipped() throws IOException { + // Test files with empty names are skipped + String content = + endline + "--" + boundary + endline + + "Content-Disposition: form-data; name=\"emptyfile\"; filename=\"\"" + endline + + "Content-Type: text/plain" + endline + + endline + + "This should be skipped" + + endline + "--" + boundary + endline + + "Content-Disposition: form-data; name=\"validfile\"; filename=\"valid.txt\"" + endline + + "Content-Type: text/plain" + endline + + endline + + "This should be processed" + + endline + "--" + boundary + "--"; + + mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8)); + + // when + multiPart.parse(mockRequest, tempDir); + + // then + assertThat(multiPart.getErrors()).isEmpty(); + assertThat(multiPart.uploadedFiles).hasSize(1); + assertThat(multiPart.getFile("emptyfile")).isEmpty(); + assertThat(multiPart.getFile("validfile")).hasSize(1); + } + }