13

I create a file upload service with Spring Boot and test it with Spring Mock Mvc and MockMultipartFile. I want to test if an error is thrown when the maximum file size is exceeded. The following test fails because it receive a 200.

RandomAccessFile f = new RandomAccessFile("t", "rw");
f.setLength(1024 * 1024 * 10);
InputStream is = Channels.newInputStream(f.getChannel());

MockMultipartFile firstFile = new MockMultipartFile("data", "file1.txt", "text/plain", is);

mvc.perform(fileUpload("/files")
    .file(firstFile))
    .andExpect(status().isInternalServerError());

Is there any possibility to test the upload file size?

fabwu
  • 642
  • 6
  • 19

3 Answers3

11

According to the documentation:

If the present length of the file as returned by the length method is smaller than the newLength argument then the file will be extended. In this case, the contents of the extended portion of the file are not defined.

Try this instead:

byte[] bytes = new byte[1024 * 1024 * 10];
MockMultipartFile firstFile = new MockMultipartFile("data", "file1.txt", "text/plain", bytes);
jny
  • 8,007
  • 3
  • 37
  • 56
  • 8
    This doesn't work for me. Is it possible that the file size check is disabled with MockMVC? – fabwu Nov 13 '15 at 22:39
  • 7
    @wuethrich44: Sure seems so. I stumbled across the same problem and couldn't find an easy solution for it either. Without looking at the code for `spring-web` (or whatever is supposed to handle this), I couldn't help but notice that the actual exception (for exceeding the limit) gets thrown by some Tomcat code (*i.e.* `java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 1048576 bytes.`). So maybe Spring just delegates this check out to the underlying application server. – Priidu Neemre Dec 10 '16 at 02:38
  • 3
    @wuethrich44: And since the `MockMvc` integration tests don't really include an actual application server... – Priidu Neemre Dec 10 '16 at 02:38
  • 2
    @PriiduNeemre Ok when Tomcat handle this error it seems impossible to test that in `MockMvc`. Strangely enough @David Jones had success with this solution... – fabwu Dec 10 '16 at 09:13
  • 2
    David is probably doing manual size checking as well as/instead of using the Spring properties. – Ashley Jul 13 '17 at 16:04
  • Any luck with testing the exception `FileSizeLimitExceededException`? – swapab Jul 27 '18 at 08:30
5

You cannot test this with Spring's MockMultipartFile / MockMvc. The reason for this is that the error's origin is not in Spring itself, but in the underlying web server (most often Tomcat), as you can see in the MaxUploadSizeExceededException's stack trace:

org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size of 500000 bytes exceeded; nested exception is org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1065736) exceeds the configured maximum (500000)
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:160)
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:139)
[...]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Unknown Source)
Caused by: org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1065736) exceeds the configured maximum (500000)
    at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:965)
    at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:310)
    at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:334)
    at org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:115)
    at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:156)
    ... 20 more

When using default MockMvc and @SpringBootTest with its default settings, no real web server is started and thus the error does not occur.

You can however tell Spring to start a real web server for your test by supplying @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT), which will (surprise) start a web server on a random port. You can access that port in your test class with @LocalServerPort.

You can then write a test that performs a real multipart upload against your test server and not a faked one. REST Assured is a library, among others, that can do this:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyTest {

    @LocalServerPort
    private int port;

    @Test
    void testMultipartUpload() throws Exception {
        File file = new File("my-file");
        RestAssured.baseURI = "http://localhost/api";
        RestAssured.port = port;
        Response res = given()
                .multiPart("data", file, "text/plain")
                .when().post("/upload");
        ...
    }

}

This test will reveal the server error when your upload is too large.

Moritz
  • 1,954
  • 2
  • 18
  • 28
  • rest assured broke my entire application by injecting groovy into an app that had nothing to do with groovy – notacorn Mar 31 '22 at 04:56
1

Had the same issue here.

Don't know if there's a better solution for this, but I created an annotation to validate a list of images uploaded and made the check there along with others.

  • Class that validate's images
    public class ImageValidator implements ConstraintValidator<ValidImage, List<MultipartFile>> {

      @Override
      public boolean isValid(
          List<MultipartFile> listMultipartFile, ConstraintValidatorContext context) {

        for (var multipartFile : listMultipartFile) {
          var msgValidation = imageValidations(multipartFile);

          if (!msgValidation.isEmpty()) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(msgValidation).addConstraintViolation();
            return false;
          }
        }

        return true;
      }

      private String imageValidations(MultipartFile multipartFile) {
        var contentType = multipartFile.getContentType();

        if (!isSupportedContentType(contentType)) {
          return String.format(
              "Only JPG and PNG images are allowed, %s provided.", multipartFile.getContentType());
        } else if (multipartFile.isEmpty()) {
          return "It must not be an empty image.";
        } else if (multipartFile.getSize() > (1024 * 1024)) {
          return "File size should be at most 1MB";
        }

        return "";
      }

      private boolean isSupportedContentType(String contentType) {
        var supportedContents = List.of("image/jpg", "image/jpeg", "image/png");
        return supportedContents.contains(contentType);
      }
    }
  • Interface
    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = {ImageValidator.class})
    public @interface ValidImage {
      String message() default "Invalid image file";

      Class<?>[] groups() default {};

      Class<? extends Payload>[] payload() default {};
    }
Wall-E
  • 438
  • 1
  • 6
  • 18