AbstractMultiPartRequestTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.struts2.dispatcher.multipart;

import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.apache.struts2.dispatcher.LocalizedMessage;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static org.assertj.core.api.Assertions.assertThat;

abstract class AbstractMultiPartRequestTest {

    protected static String tempDir;

    protected MockHttpServletRequest mockRequest;

    protected final String boundary = "_boundary_";
    protected final String endline = "\r\n";

    protected AbstractMultiPartRequest multiPart;

    abstract protected AbstractMultiPartRequest createMultipartRequest();

    @BeforeClass
    public static void beforeClass() throws IOException {
        File tempFile = File.createTempFile("struts", "fileupload");
        assertThat(tempFile.delete()).isTrue();
        assertThat(tempFile.mkdirs()).isTrue();
        tempDir = tempFile.getAbsolutePath();
    }

    @Before
    public void before() {
        mockRequest = new MockHttpServletRequest();
        mockRequest.setCharacterEncoding(StandardCharsets.UTF_8.name());
        mockRequest.setMethod("post");
        mockRequest.setContentType("multipart/form-data; boundary=" + boundary);

        multiPart = createMultipartRequest();
    }

    @After
    public void after() {
        multiPart.cleanUp();
    }

    @Test
    public void uploadedFilesToDisk() throws IOException {
        // given
        String content = formFile("file1", "test1.csv", "1,2,3,4") +
                formFile("file2", "test2.csv", "5,6,7,8") +
                endline + "--" + boundary + "--";

        mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));

        assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();

        // when
        multiPart.setBufferSize("1"); // always write files into disk
        multiPart.parse(mockRequest, tempDir);

        // then
        assertThat(multiPart.getErrors())
                .isEmpty();

        assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
                .asInstanceOf(InstanceOfAssertFactories.LIST)
                .containsOnly("file1", "file2");
        assertThat(multiPart.getFile("file1")).allSatisfy(file -> {
            assertThat(file.isFile())
                    .isTrue();
            assertThat(file.getOriginalName())
                    .isEqualTo("test1.csv");
            assertThat(file.getContentType())
                    .isEqualTo("text/csv");
            assertThat(file.getInputName())
                    .isEqualTo("file1");
            assertThat(file.getContent()).asInstanceOf(InstanceOfAssertFactories.FILE)
                    .exists()
                    .content()
                    .isEqualTo("1,2,3,4");
        });
        assertThat(multiPart.getFile("file2")).allSatisfy(file -> {
            assertThat(file.isFile())
                    .isTrue();
            assertThat(file.getOriginalName())
                    .isEqualTo("test2.csv");
            assertThat(file.getContentType())
                    .isEqualTo("text/csv");
            assertThat(file.getInputName())
                    .isEqualTo("file2");
            assertThat(file.getContent())
                    .asInstanceOf(InstanceOfAssertFactories.FILE)
                    .exists()
                    .content()
                    .isEqualTo("5,6,7,8");
        });
    }

    @Test
    public void uploadedMultipleFilesToDisk() throws IOException {
        // given
        String content = formFile("file1", "test1.csv", "1,2,3,4") +
                formFile("file1", "test2.csv", "5,6,7,8") +
                endline + "--" + boundary + "--";

        mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));

        assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();

        // when
        multiPart.setBufferSize("1"); // always write files into disk
        multiPart.parse(mockRequest, tempDir);

        // then
        assertThat(multiPart.getErrors())
                .isEmpty();

        assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
                .asInstanceOf(InstanceOfAssertFactories.LIST)
                .containsOnly("file1");
        assertThat(multiPart.getFile("file1")).allSatisfy(file -> {
            if (Objects.equals(file.getName(), "test1.csv")) {
                assertThat(file.isFile())
                        .isTrue();
                assertThat(file.getOriginalName())
                        .isEqualTo("test1.csv");
                assertThat(file.getContentType())
                        .isEqualTo("text/csv");
                assertThat(file.getInputName())
                        .isEqualTo("file1");
                assertThat(file.getContent()).asInstanceOf(InstanceOfAssertFactories.FILE)
                        .exists()
                        .content()
                        .isEqualTo("1,2,3,4");
            }
            if (Objects.equals(file.getName(), "test2.csv")) {
                assertThat(file.isFile())
                        .isTrue();
                assertThat(file.getOriginalName())
                        .isEqualTo("test2.csv");
                assertThat(file.getContentType())
                        .isEqualTo("text/csv");
                assertThat(file.getInputName())
                        .isEqualTo("file1");
                assertThat(file.getContent())
                        .asInstanceOf(InstanceOfAssertFactories.FILE)
                        .exists()
                        .content()
                        .isEqualTo("5,6,7,8");
            }
        });
    }

    @Test
    public void uploadedFilesWithLargeBuffer() throws IOException {
        // given
        String content = formFile("file1", "test1.csv", "1,2,3,4") +
                formFile("file2", "test2.csv", "5,6,7,8") +
                endline + "--" + boundary + "--";

        mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));

        assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();

        // when
        multiPart.setBufferSize("8192"); // streams files into disk using larger buffer
        multiPart.parse(mockRequest, tempDir);

        // then
        assertThat(multiPart.getErrors())
                .isEmpty();

        assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
                .asInstanceOf(InstanceOfAssertFactories.LIST)
                .containsOnly("file1", "file2");
        assertThat(multiPart.getFile("file1")).allSatisfy(file -> {
            assertThat(file.isFile())
                    .isTrue();
            assertThat(file.getOriginalName())
                    .isEqualTo("test1.csv");
            assertThat(file.getContentType())
                    .isEqualTo("text/csv");
            assertThat(file.getInputName())
                    .isEqualTo("file1");
            assertThat(file.getContent())
                    .asInstanceOf(InstanceOfAssertFactories.FILE)
                    .exists()
                    .content()
                    .isEqualTo("1,2,3,4");
        });
        assertThat(multiPart.getFile("file2")).allSatisfy(file -> {
            assertThat(file.isFile())
                    .isTrue();
            assertThat(file.getOriginalName())
                    .isEqualTo("test2.csv");
            assertThat(file.getInputName())
                    .isEqualTo("file2");
            assertThat(file.getContent())
                    .asInstanceOf(InstanceOfAssertFactories.FILE)
                    .exists()
                    .content()
                    .isEqualTo("5,6,7,8");
        });
    }

    @Test
    public void cleanUp() throws IOException {
        // given
        String content = formFile("file1", "test1.csv", "1,2,3,4") +
                formFile("file2", "test2.csv", "5,6,7,8") +
                endline + "--" + boundary + "--";

        mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));

        assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();

        // when
        multiPart.parse(mockRequest, tempDir);

        // then
        assertThat(multiPart.getErrors())
                .isEmpty();

        assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
                .asInstanceOf(InstanceOfAssertFactories.LIST)
                .containsOnly("file1", "file2");
        assertThat(multiPart.getFile("file1")).allSatisfy(file -> {
            assertThat(file.isFile())
                    .isTrue();
            assertThat(file.getOriginalName())
                    .isEqualTo("test1.csv");
            assertThat(file.getContentType())
                    .isEqualTo("text/csv");
            assertThat(file.getInputName())
                    .isEqualTo("file1");
            assertThat(file.getContent()).asInstanceOf(InstanceOfAssertFactories.FILE)
                    .exists()
                    .content()
                    .isEqualTo("1,2,3,4");
        });
        assertThat(multiPart.getFile("file2")).allSatisfy(file -> {
            assertThat(file.isFile())
                    .isTrue();
            assertThat(file.getOriginalName())
                    .isEqualTo("test2.csv");
            assertThat(file.getContentType())
                    .isEqualTo("text/csv");
            assertThat(file.getInputName())
                    .isEqualTo("file2");
            assertThat(file.getContent())
                    .asInstanceOf(InstanceOfAssertFactories.FILE)
                    .exists()
                    .content()
                    .isEqualTo("5,6,7,8");
        });

        List<UploadedFile> uploadedFiles = new ArrayList<>();
        for (Map.Entry<String, List<UploadedFile>> entry : multiPart.uploadedFiles.entrySet()) {
            uploadedFiles.addAll(entry.getValue());
        }

        // when
        multiPart.cleanUp();

        // then
        assertThat(multiPart.uploadedFiles)
                .isEmpty();
        assertThat(multiPart.parameters)
                .isEmpty();
        assertThat(uploadedFiles).allSatisfy(file ->
                assertThat(file.getContent()).asInstanceOf(InstanceOfAssertFactories.FILE)
                        .doesNotExist()
        );
    }

    @Test
    public void nonMultiPartUpload() throws IOException {
        String content = formFile("file1", "test1.csv", "1,2,3,4") +
                formFile("file2", "test2.csv", "5,6,7,8") +
                endline + "--" + boundary + "--";
        mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));

        // given
        mockRequest.setContentType("");

        // when
        multiPart.parse(mockRequest, tempDir);

        // then
        assertThat(multiPart.getErrors())
                .map(LocalizedMessage::getTextKey)
                .containsExactly("struts.messages.upload.error.FileUploadContentTypeException");

        assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
                .asInstanceOf(InstanceOfAssertFactories.LIST)
                .isEmpty();
    }

    @Test
    public void maxSize() throws IOException {
        // given
        String content = formFile("file1", "test1.csv", "1,2,3,4") +
                formFile("file2", "test2.csv", "5,6,7,8") +
                endline + "--" + boundary + "--";

        mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));

        assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();

        // when
        multiPart.setMaxSize("1");
        multiPart.parse(mockRequest, tempDir);

        // then
        assertThat(multiPart.uploadedFiles)
                .isEmpty();

        assertThat(multiPart.getErrors())
                .map(LocalizedMessage::getTextKey)
                .containsExactly("struts.messages.upload.error.FileUploadSizeException");
    }

    @Test
    public void maxFilesSize() throws IOException {
        String content = formFile("file1", "test1.csv", "1,2,3,4") +
                formFile("file2", "test2.csv", "5,6,7,8") +
                endline + "--" + boundary + "--";

        mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));

        assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();

        multiPart.setMaxFileSize("1");
        multiPart.parse(mockRequest, tempDir);

        assertThat(multiPart.getErrors())
                .map(LocalizedMessage::getTextKey)
                .containsExactly("struts.messages.upload.error.FileUploadByteCountLimitException");
    }

    @Test
    public void maxFiles() throws IOException {
        String content = formFile("file1", "test1.csv", "1,2,3,4") +
                formFile("file2", "test2.csv", "5,6,7,8") +
                endline + "--" + boundary + "--";

        mockRequest.setContent(content.getBytes(StandardCharsets.US_ASCII));

        assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();

        multiPart.setMaxFiles("1");
        multiPart.parse(mockRequest, tempDir);

        assertThat(multiPart.errors)
                .map(LocalizedMessage::getTextKey)
                .containsExactly("struts.messages.upload.error.FileUploadFileCountLimitException");
    }

    @Test
    public void maxStringLength() throws IOException {
        String content = formFile("file1", "test1.csv", "1,2,3,4") +
                formFile("file2", "test2.csv", "5,6,7,8") +
                formField("longText", "very long text") +
                formField("shortText", "short text") +
                endline + "--" + boundary + "--";

        mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));

        assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();

        multiPart.setMaxStringLength("10");
        multiPart.parse(mockRequest, tempDir);

        assertThat(multiPart.getErrors())
                .map(LocalizedMessage::getTextKey)
                .containsExactly("struts.messages.upload.error.parameter.too.long");
    }

    @Test
    public void mismatchCharset() throws IOException {
        // give
        String content = formFile("file1", "test1.csv", "��,��,��,��") +
                endline + "--" + boundary + "--";

        mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));

        assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();

        // when
        multiPart.setDefaultEncoding(StandardCharsets.ISO_8859_1.name());
        multiPart.parse(mockRequest, tempDir);

        // then
        assertThat(multiPart.getErrors())
                .isEmpty();

        assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
                .asInstanceOf(InstanceOfAssertFactories.LIST)
                .containsOnly("file1");
        assertThat(multiPart.getFile("file1")).allSatisfy(file -> {
            assertThat(file.isFile())
                    .isTrue();
            assertThat(file.getOriginalName())
                    .isEqualTo("test1.csv");
            assertThat(file.getContentType())
                    .isEqualTo("text/csv");
            assertThat(file.getContent())
                    .asInstanceOf(InstanceOfAssertFactories.FILE)
                    .exists()
                    .content()
                    .isEqualTo("��,��,��,��");
        });
    }

    @Test
    public void normalFields() throws IOException {
        String content = formFile("file1", "test1.csv", "1,2,3,4") +
                formFile("file2", "test2.csv", "5,6,7,8") +
                formField("longText", "very long text") +
                formField("shortText", "short text") +
                formField("multi", "multi1") +
                formField("multi", "multi2") +
                endline + "--" + boundary + "--";

        mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));

        assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();

        multiPart.parse(mockRequest, tempDir);

        assertThat(multiPart.getErrors())
                .isEmpty();

        assertThat(multiPart.getParameterNames().asIterator()).toIterable()
                .containsOnly("longText", "shortText", "multi");
        assertThat(multiPart.getParameterValues("longText"))
                .contains("very long text");
        assertThat(multiPart.getParameterValues("shortText"))
                .contains("short text");
        assertThat(multiPart.getParameter("longText"))
                .isEqualTo("very long text");
        assertThat(multiPart.getParameter("shortText"))
                .isEqualTo("short text");
        assertThat(multiPart.getParameterValues("multi"))
                .containsOnly("multi1", "multi2");
        assertThat(multiPart.getParameterValues("not-existing"))
                .isNull();
    }

    @Test
    public void unableParseRequest() throws IOException {
        String content = formFile("file1", "test1.csv", "1,2,3,4");
        mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));

        assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();

        multiPart.parse(mockRequest, tempDir);

        assertThat(multiPart.getErrors())
                .map(LocalizedMessage::getTextKey)
                .containsExactly("struts.messages.upload.error.FileUploadException");
    }

    protected String formFile(String fieldName, String filename, String content) {
        return endline +
                "--" + boundary + endline +
                "Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + filename + "\"" +
                endline +
                "Content-Type: text/csv" +
                endline +
                endline +
                content;
    }

    protected String formField(String fieldName, String content) {
        return endline +
                "--" + boundary + endline +
                "Content-Disposition: form-data; name=\"" + fieldName + "\"" +
                endline +
                endline +
                content;
    }
}