MultipartPartTest.java

/*
 *    Copyright (c) 2015-2023 AsyncHttpClient Project. All rights reserved.
 *
 *    Licensed 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.asynchttpclient.request.body.multipart.part;

import io.github.artsok.RepeatedIfExceptionsTest;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpHeaders;
import org.apache.commons.io.FileUtils;
import org.asynchttpclient.request.body.multipart.FileLikePart;
import org.asynchttpclient.request.body.multipart.MultipartBody;
import org.asynchttpclient.request.body.multipart.MultipartUtils;
import org.asynchttpclient.request.body.multipart.Part;
import org.asynchttpclient.request.body.multipart.StringPart;
import org.asynchttpclient.request.body.multipart.part.PartVisitor.CounterPartVisitor;
import org.asynchttpclient.test.TestUtils;

import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class MultipartPartTest {

    public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitStart() {
        TestFileLikePart fileLikePart = new TestFileLikePart("Name");
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, new byte[10])) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitStart(counterVisitor);
            assertEquals(12, counterVisitor.getCount(), "CounterPartVisitor count for visitStart should match EXTRA_BYTES count plus boundary bytes count");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitStartZeroSizedByteArray() {
        TestFileLikePart fileLikePart = new TestFileLikePart("Name");
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitStart(counterVisitor);
            assertEquals(2, counterVisitor.getCount(), "CounterPartVisitor count for visitStart should match EXTRA_BYTES count when boundary byte array is of size zero");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitDispositionHeaderWithoutFileName() {
        TestFileLikePart fileLikePart = new TestFileLikePart("Name");
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitDispositionHeader(counterVisitor);
            assertEquals(45, counterVisitor.getCount(), "CounterPartVisitor count for visitDispositionHeader should be equal to "
                    + "CRLF_BYTES length + CONTENT_DISPOSITION_BYTES length + part name length when file name is not specified");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitDispositionHeaderWithFileName() {
        TestFileLikePart fileLikePart = new TestFileLikePart("baPart", null, null, null, null, "fileName");
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitDispositionHeader(counterVisitor);
            assertEquals(68, counterVisitor.getCount(), "CounterPartVisitor count for visitDispositionHeader should be equal to "
                    + "CRLF_BYTES length + CONTENT_DISPOSITION_BYTES length + part name length + file name length when" + " both part name and file name are present");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitDispositionHeaderWithoutName() {
        // with fileName
        TestFileLikePart fileLikePart = new TestFileLikePart(null, null, null, null, null, "fileName");
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitDispositionHeader(counterVisitor);
            assertEquals(53, counterVisitor.getCount(), "CounterPartVisitor count for visitDispositionHeader should be equal to "
                    + "CRLF_BYTES length + CONTENT_DISPOSITION_BYTES length + file name length when part name is not specified");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitContentTypeHeaderWithCharset() {
        TestFileLikePart fileLikePart = new TestFileLikePart(null, "application/test", UTF_8, null, null);
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitContentTypeHeader(counterVisitor);
            assertEquals(47, counterVisitor.getCount(), "CounterPartVisitor count for visitContentTypeHeader should be equal to "
                    + "CRLF_BYTES length + CONTENT_TYPE_BYTES length + contentType length + charset length");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitContentTypeHeaderWithoutCharset() {
        TestFileLikePart fileLikePart = new TestFileLikePart(null, "application/test");
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitContentTypeHeader(counterVisitor);
            assertEquals(32, counterVisitor.getCount(), "CounterPartVisitor count for visitContentTypeHeader should be equal to "
                    + "CRLF_BYTES length + CONTENT_TYPE_BYTES length + contentType length when charset is not specified");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitTransferEncodingHeader() {
        TestFileLikePart fileLikePart = new TestFileLikePart(null, null, null, null, "transferEncoding");
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitTransferEncodingHeader(counterVisitor);
            assertEquals(45, counterVisitor.getCount(), "CounterPartVisitor count for visitTransferEncodingHeader should be equal to "
                    + "CRLF_BYTES length + CONTENT_TRANSFER_ENCODING_BYTES length + transferEncoding length");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitContentIdHeader() {
        TestFileLikePart fileLikePart = new TestFileLikePart(null, null, null, "contentId");
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitContentIdHeader(counterVisitor);
            assertEquals(23, counterVisitor.getCount(), "CounterPartVisitor count for visitContentIdHeader should be equal to"
                    + "CRLF_BYTES length + CONTENT_ID_BYTES length + contentId length");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitCustomHeadersWhenNoCustomHeaders() {
        TestFileLikePart fileLikePart = new TestFileLikePart(null);
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitCustomHeaders(counterVisitor);
            assertEquals(0, counterVisitor.getCount(), "CounterPartVisitor count for visitCustomHeaders should be zero for visitCustomHeaders "
                    + "when there are no custom headers");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitCustomHeaders() {
        TestFileLikePart fileLikePart = new TestFileLikePart(null);
        fileLikePart.addCustomHeader("custom-header", "header-value");
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitCustomHeaders(counterVisitor);
            assertEquals(29, counterVisitor.getCount(), "CounterPartVisitor count for visitCustomHeaders should include the length of the custom headers");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitEndOfHeaders() {
        TestFileLikePart fileLikePart = new TestFileLikePart(null);
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitEndOfHeaders(counterVisitor);
            assertEquals(4, counterVisitor.getCount(), "CounterPartVisitor count for visitEndOfHeaders should be equal to 4");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitPreContent() {
        TestFileLikePart fileLikePart = new TestFileLikePart("Name", "application/test", UTF_8, "contentId", "transferEncoding", "fileName");
        fileLikePart.addCustomHeader("custom-header", "header-value");
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitPreContent(counterVisitor);
            assertEquals(216, counterVisitor.getCount(), "CounterPartVisitor count for visitPreContent should " + "be equal to the sum of the lengths of precontent");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void testVisitPostContents() {
        TestFileLikePart fileLikePart = new TestFileLikePart(null);
        try (TestMultipartPart multipartPart = new TestMultipartPart(fileLikePart, EMPTY_BYTE_ARRAY)) {
            CounterPartVisitor counterVisitor = new CounterPartVisitor();
            multipartPart.visitPostContent(counterVisitor);
            assertEquals(2, counterVisitor.getCount(), "CounterPartVisitor count for visitPostContent should be equal to 2");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void transferToShouldWriteStringPart() throws Exception {
        String text = FileUtils.readFileToString(TestUtils.resourceAsFile("test_sample_message.eml"), UTF_8);

        List<Part> parts = new ArrayList<>();
        parts.add(new StringPart("test_sample_message.eml", text));

        HttpHeaders headers = new DefaultHttpHeaders();
        headers.set("Cookie",
                "open-xchange-public-session-d41d8cd98f00b204e9800998ecf8427e=bfb98150b24f42bd844fc9ef2a9eaad5; open-xchange-secret-TSlq4Cm4nCBnDpBL1Px2A=9a49b76083e34c5ba2ef5c47362313fd; JSESSIONID=6883138728830405130.OX2");
        headers.set("Content-Length", "9241");
        headers.set("Content-Type", "multipart/form-data; boundary=5gigAKQyqDCVdlZ1fCkeLlEDDauTNoOOEhRnFg");
        headers.set("Host", "appsuite.qa.open-xchange.com");
        headers.set("Accept", "*/*");

        String boundary = "uwyqQolZaSmme019O2kFKvAeHoC14Npp";

        List<MultipartPart<? extends Part>> multipartParts = MultipartUtils.generateMultipartParts(parts, boundary.getBytes());
        try (MultipartBody multipartBody = new MultipartBody(multipartParts, "multipart/form-data; boundary=" + boundary, boundary.getBytes())) {

            ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(8 * 1024);
            multipartBody.transferTo(byteBuf);
            try {
                byteBuf.toString(UTF_8);
            } finally {
                byteBuf.release();
            }
        }
    }

    /**
     * Concrete implementation of {@link FileLikePart} for use in unit tests
     */
    private static class TestFileLikePart extends FileLikePart {

        TestFileLikePart(String name) {
            this(name, null, null, null, null);
        }

        TestFileLikePart(String name, String contentType) {
            this(name, contentType, null);
        }

        TestFileLikePart(String name, String contentType, Charset charset) {
            this(name, contentType, charset, null);
        }

        TestFileLikePart(String name, String contentType, Charset charset, String contentId) {
            this(name, contentType, charset, contentId, null);
        }

        TestFileLikePart(String name, String contentType, Charset charset, String contentId, String transferEncoding) {
            this(name, contentType, charset, contentId, transferEncoding, null);
        }

        TestFileLikePart(String name, String contentType, Charset charset, String contentId, String transferEncoding, String fileName) {
            super(name, contentType, charset, fileName, contentId, transferEncoding);
        }
    }

    /**
     * Concrete implementation of MultipartPart for use in unit tests.
     */
    private static class TestMultipartPart extends FileLikeMultipartPart<TestFileLikePart> {

        TestMultipartPart(TestFileLikePart part, byte[] boundary) {
            super(part, boundary);
        }

        @Override
        protected long getContentLength() {
            return 0;
        }

        @Override
        protected long transferContentTo(ByteBuf target) {
            return 0;
        }

        @Override
        protected long transferContentTo(WritableByteChannel target) {
            return 0;
        }
    }
}