com.linecorp.armeria.server.AnnotatedHttpServiceRequestConverterTest.java Source code

Java tutorial

Introduction

Here is the source code for com.linecorp.armeria.server.AnnotatedHttpServiceRequestConverterTest.java

Source

/*
 * Copyright 2017 LINE Corporation
 *
 * LINE Corporation 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:
 *
 *   https://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 com.linecorp.armeria.server;

import static com.linecorp.armeria.server.AnnotatedHttpServiceRequestConverterTest.Gender.MALE;
import static com.linecorp.armeria.server.annotation.BeanRequestConverterFunction.register;
import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import javax.annotation.Nullable;

import org.junit.ClassRule;
import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import com.linecorp.armeria.client.HttpClient;
import com.linecorp.armeria.common.AggregatedHttpMessage;
import com.linecorp.armeria.common.HttpData;
import com.linecorp.armeria.common.HttpHeaders;
import com.linecorp.armeria.common.HttpMethod;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.MediaType;
import com.linecorp.armeria.server.TestConverters.ByteArrayConverterFunction;
import com.linecorp.armeria.server.TestConverters.UnformattedStringConverterFunction;
import com.linecorp.armeria.server.annotation.Get;
import com.linecorp.armeria.server.annotation.Header;
import com.linecorp.armeria.server.annotation.Param;
import com.linecorp.armeria.server.annotation.Post;
import com.linecorp.armeria.server.annotation.RequestConverter;
import com.linecorp.armeria.server.annotation.RequestConverterFunction;
import com.linecorp.armeria.server.annotation.RequestObject;
import com.linecorp.armeria.server.annotation.ResponseConverter;
import com.linecorp.armeria.server.logging.LoggingService;
import com.linecorp.armeria.testing.server.ServerRule;

import io.netty.util.AsciiString;

public class AnnotatedHttpServiceRequestConverterTest {

    @ClassRule
    public static final ServerRule rule = new ServerRule() {
        @Override
        protected void configure(ServerBuilder sb) throws Exception {
            sb.annotatedService("/1", new MyService1(), LoggingService.newDecorator());
            sb.annotatedService("/2", new MyService2(), LoggingService.newDecorator());
        }
    };

    @ResponseConverter(UnformattedStringConverterFunction.class)
    @RequestConverter(TestRequestConverter1.class)
    public static class MyService1 {

        @Post("/convert1")
        public String convert1(@RequestObject RequestJsonObj1 obj1) {
            assertThat(obj1).isNotNull();
            return obj1.toString();
        }

        @Post("/convert2")
        @RequestConverter(TestRequestConverter2.class)
        @RequestConverter(TestRequestConverter1A.class)
        @RequestConverter(TestRequestConverter1.class)
        public String convert2(@RequestObject RequestJsonObj1 obj1) {
            assertThat(obj1).isNotNull();
            return obj1.toString();
        }

        @Post("/convert3")
        public String convert3(@RequestObject(TestRequestConverterOptional1.class) Optional<RequestJsonObj1> obj1,
                @RequestObject(TestRequestConverterOptional2.class) Optional<RequestJsonObj2> obj2) {
            assertThat(obj1.isPresent()).isTrue();
            assertThat(obj2.isPresent()).isTrue();
            return obj2.get().strVal();
        }
    }

    @ResponseConverter(ByteArrayConverterFunction.class)
    @ResponseConverter(UnformattedStringConverterFunction.class)
    public static class MyService2 {
        private final ObjectMapper mapper = new ObjectMapper();

        @Post("/default/bean1/{userName}/{seqNum}")
        public String defaultBean1ForPost(@RequestObject RequestBean1 bean1) throws JsonProcessingException {
            assertThat(bean1).isNotNull();
            bean1.validate();
            return mapper.writeValueAsString(bean1);
        }

        @Get("/default/bean1/{userName}/{seqNum}")
        public String defaultBean1ForGet(@RequestObject RequestBean1 bean1) throws JsonProcessingException {
            assertThat(bean1).isNotNull();
            bean1.validate();
            return mapper.writeValueAsString(bean1);
        }

        @Post("/default/bean2/{userName}/{serialNo}")
        public String defaultBean2ForPost(@RequestObject RequestBean2 bean2) throws JsonProcessingException {
            assertThat(bean2).isNotNull();
            bean2.validate();
            return mapper.writeValueAsString(bean2);
        }

        @Get("/default/bean2/{userName}")
        public String defaultBean2ForGet(@RequestObject RequestBean2 bean2) throws JsonProcessingException {
            assertThat(bean2).isNotNull();
            bean2.validate();
            return mapper.writeValueAsString(bean2);
        }

        @Post("/default/bean3/{userName}/{departmentNo}")
        public String defaultBean3ForPost(@RequestObject RequestBean3 bean3) throws JsonProcessingException {
            assertThat(bean3).isNotNull();
            bean3.validate();
            return mapper.writeValueAsString(bean3);
        }

        @Get("/default/bean3/{userName}")
        public String defaultBean3ForGet(@RequestObject RequestBean3 bean3) throws JsonProcessingException {
            assertThat(bean3).isNotNull();
            bean3.validate();
            return mapper.writeValueAsString(bean3);
        }

        @Post("/default/json")
        public String defaultJson(@RequestObject RequestJsonObj1 obj1, @RequestObject RequestJsonObj2 obj2) {
            assertThat(obj1).isNotNull();
            assertThat(obj2).isNotNull();
            return obj2.strVal();
        }

        @Post("/default/invalidJson")
        public String invalidJson(@RequestObject JsonNode node) {
            // Should never reach here because we are sending invalid JSON.
            throw new Error();
        }

        @Post("/default/binary")
        public byte[] defaultBinary(@RequestObject HttpData obj1, @RequestObject byte[] obj2) {
            assertThat(obj1).isNotNull();
            assertThat(obj2).isNotNull();
            // Actually they have the same byte array.
            assertThat(obj1.array()).isSameAs(obj2);
            return obj2;
        }

        @Post("/default/text")
        public String defaultText(@RequestObject String obj1) {
            assertThat(obj1).isNotNull();
            return obj1;
        }
    }

    static class RequestJsonObj1 {
        private final int intVal;
        private final String strVal;

        @JsonCreator
        RequestJsonObj1(@JsonProperty("intVal") int intVal, @JsonProperty("strVal") String strVal) {
            this.intVal = intVal;
            this.strVal = requireNonNull(strVal, "strVal");
        }

        @JsonProperty
        int intVal() {
            return intVal;
        }

        @JsonProperty
        String strVal() {
            return strVal;
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() + ':' + intVal() + ':' + strVal();
        }
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    static class RequestJsonObj2 {
        private final String strVal;

        @JsonCreator
        RequestJsonObj2(@JsonProperty("strVal") String strVal) {
            this.strVal = requireNonNull(strVal, "strVal");
        }

        @JsonProperty
        String strVal() {
            return strVal;
        }
    }

    abstract static class AbstractRequestBean {
        // test case: field with annotation
        @Nullable
        @Param
        String userName;

        int age = Integer.MIN_VALUE;

        @Nullable
        Gender gender;

        @Nullable
        List<String> permissions;

        @Nullable
        String clientName;

        // test case: method parameters with annotation
        public void initParams(@Param("age") final int age, @Header("x-client-name") final String clientName,
                @Header final String xUserPermission) {
            this.age = age;
            this.clientName = clientName;
            permissions = Arrays.asList(xUserPermission.split(","));
        }

        @JsonProperty
        public int getAge() {
            return age;
        }

        @JsonProperty
        public Gender getGender() {
            return gender;
        }

        // test case: method with annotation
        @Param
        public void setGender(final Gender gender) {
            this.gender = gender;
        }

        @JsonProperty
        public String getUserName() {
            return userName;
        }

        @JsonProperty
        public List<String> getPermissions() {
            return permissions;
        }

        @JsonProperty
        public String getClientName() {
            return clientName;
        }

        public void validate() {
            assertThat(userName).isNotNull();
            assertThat(age).isNotEqualTo(Integer.MIN_VALUE);
            assertThat(gender).isNotNull();
            assertThat(permissions).isNotNull();
            assertThat(clientName).isNotNull();
        }
    }

    enum Gender {
        MALE, FEMALE
    }

    // test case: default constructor(with no args)
    static class RequestBean1 extends AbstractRequestBean {
        // test case: field with annotation
        @Nullable
        @Param("seqNum")
        private Long seqNum;

        // test case: field with annotation
        @Nullable
        @Param("manager")
        private Boolean manager;

        @Nullable
        private String notPopulatedStr;

        private int notPopulatedInt;

        @Nullable
        private Long notPopulatedLong;

        @Nullable
        private Boolean notPopulatedBoolean;

        @JsonProperty
        public long getSeqNum() {
            return seqNum;
        }

        @JsonProperty
        public boolean isManager() {
            return manager;
        }

        @Override
        public void validate() {
            super.validate();

            assertThat(seqNum).isNotNull();
            assertThat(manager).isNotNull();

            assertThat(notPopulatedStr).isNull();
            assertThat(notPopulatedInt).isEqualTo(0);
            assertThat(notPopulatedLong).isNull();
            assertThat(notPopulatedBoolean).isNull();
        }
    }

    static class RequestBean2 extends AbstractRequestBean {
        // test case: field with annotation
        @Param("serialNo")
        private final Long serialNo;

        private final String uid;

        // test case: constructor args with annotations
        RequestBean2(@Param("serialNo") Long serialNo, @Header("uid") String uid) {
            this.serialNo = serialNo;
            this.uid = uid;
        }

        @Override
        public void validate() {
            super.validate();

            assertThat(serialNo).isNotNull();
            assertThat(uid).isNotNull();
        }
    }

    static class RequestBean3 extends AbstractRequestBean {
        private int departmentNo = Integer.MIN_VALUE;

        // test case: constructor with annotations
        @Param("departmentNo")
        RequestBean3(int departmentNo) {
            this.departmentNo = departmentNo;
        }

        @Override
        public void validate() {
            super.validate();

            assertThat(departmentNo).isNotEqualTo(Integer.MIN_VALUE);
        }
    }

    // error test case: more than 1 annotated constructors
    static class BadRequestBeanMoreThanOnConstructor01 {
        private String param1;
        private int header2;

        // constructor1: with annotation
        @Param("param1")
        BadRequestBeanMoreThanOnConstructor01(String param1) {
            this.param1 = param1;
        }

        // constructor2: with annotation
        @Header("header2")
        BadRequestBeanMoreThanOnConstructor01(int header2) {
            this.header2 = header2;
        }
    }

    // error test case: more than 1 annotated constructors
    static class BadRequestBeanMoreThanOnConstructor02 {
        private String param1;
        private int header2;

        // constructor1: param with annotation
        BadRequestBeanMoreThanOnConstructor02(@Param("param1") String param1) {
            this.param1 = param1;
        }

        // constructor2: param with annotation
        BadRequestBeanMoreThanOnConstructor02(@Header("header2") int header2) {
            this.header2 = header2;
        }
    }

    // error test case: more than 1 annotated constructors
    static class BadRequestBeanMoreThanOnConstructor03 {
        private String param1;
        private int header2;

        // constructor1: with annotation
        @Param("param1")
        BadRequestBeanMoreThanOnConstructor03(String param1) {
            this.param1 = param1;
        }

        // constructor2: param with annotation
        BadRequestBeanMoreThanOnConstructor03(@Header("header2") int header2) {
            this.header2 = header2;
        }
    }

    // error test case: annotated used both on constructor and parameter
    static class BadRequestBeanAnnotationInConstructorParam {
        private final int header2;

        @Header("header2")
        BadRequestBeanAnnotationInConstructorParam(@Param("header2") int header2) {
            this.header2 = header2;
        }
    }

    // error test case: annotated used both on method and parameter
    static class BadRequestBeanAnnotationInMethodParam {
        private int header2;

        @Header("header2")
        void setHeader2(@Param("header2") int header2) {
            this.header2 = header2;
        }
    }

    // error test case: more than 1 parameters for annotated constructor
    static class BadRequestBeanMoreThanOneConstructorParam {
        private final String param1;
        private final int header2;

        @Header("header2")
        BadRequestBeanMoreThanOneConstructorParam(String param1, int header2) {
            this.param1 = param1;
            this.header2 = header2;
        }
    }

    // error test case: more than 1 parameters for annotated method
    static class BadRequestBeanMoreThanOneMethodParam {
        @Nullable
        private String param1;
        private int header2;

        @Header("header2")
        void initParams(String param1, int header2) {
            this.param1 = param1;
            this.header2 = header2;
        }
    }

    // error test case: some constructor parameters are not annotated
    static class BadRequestBeanSomeConstructorParamWithoutAnnotation {
        private final String param1;
        private final String param2;
        private final int header1;
        private final int header2;

        BadRequestBeanSomeConstructorParamWithoutAnnotation(@Param("param1") String param1, String param2,
                @Header("header1") int header1, int header2) {
            this.param1 = param1;
            this.param2 = param2;
            this.header1 = header1;
            this.header2 = header2;
        }
    }

    // error test case: some method parameters are not annotated
    static class BadRequestBeanSomeMethodParamWithoutAnnotation {
        @Nullable
        private String param1;
        @Nullable
        private String param2;
        private int header1;
        private int header2;

        void initParams(@Param("param1") String param1, String param2, @Header("header1") int header1,
                int header2) {
            this.param1 = param1;
            this.param2 = param2;
            this.header1 = header1;
            this.header2 = header2;
        }
    }

    public static class TestRequestConverter1 implements RequestConverterFunction {
        private final ObjectMapper mapper = new ObjectMapper();

        @Override
        public RequestJsonObj1 convertRequest(ServiceRequestContext ctx, AggregatedHttpMessage request,
                Class<?> expectedResultType) throws Exception {
            if (expectedResultType.isAssignableFrom(RequestJsonObj1.class)) {
                return mapper.readValue(request.content().toStringUtf8(), RequestJsonObj1.class);
            }
            return RequestConverterFunction.fallthrough();
        }
    }

    public static class TestRequestConverter1A implements RequestConverterFunction {
        private final ObjectMapper mapper = new ObjectMapper();

        @Override
        public RequestJsonObj1 convertRequest(ServiceRequestContext ctx, AggregatedHttpMessage request,
                Class<?> expectedResultType) throws Exception {
            if (expectedResultType.isAssignableFrom(RequestJsonObj1.class)) {
                final RequestJsonObj1 obj1 = mapper.readValue(request.content().toStringUtf8(),
                        RequestJsonObj1.class);
                return new RequestJsonObj1(obj1.intVal() + 1, obj1.strVal() + 'a');
            }
            return RequestConverterFunction.fallthrough();
        }
    }

    public static class TestRequestConverter2 implements RequestConverterFunction {
        @Override
        public RequestJsonObj2 convertRequest(ServiceRequestContext ctx, AggregatedHttpMessage request,
                Class<?> expectedResultType) throws Exception {
            if (expectedResultType.isAssignableFrom(RequestJsonObj2.class)) {
                return new RequestJsonObj2(request.headers().method().name());
            }
            return RequestConverterFunction.fallthrough();
        }
    }

    public static class TestRequestConverterOptional1 implements RequestConverterFunction {
        private final ObjectMapper mapper = new ObjectMapper();

        @Override
        public Optional<RequestJsonObj1> convertRequest(ServiceRequestContext ctx, AggregatedHttpMessage request,
                Class<?> expectedResultType) throws Exception {
            return Optional.of(mapper.readValue(request.content().toStringUtf8(), RequestJsonObj1.class));
        }
    }

    public static class TestRequestConverterOptional2 implements RequestConverterFunction {
        @Override
        public Optional<RequestJsonObj2> convertRequest(ServiceRequestContext ctx, AggregatedHttpMessage request,
                Class<?> expectedResultType) throws Exception {
            return Optional.of(new RequestJsonObj2(request.headers().method().name()));
        }
    }

    @Test
    public void testRequestConverter() throws Exception {
        final HttpClient client = HttpClient.of(rule.uri("/"));
        final ObjectMapper mapper = new ObjectMapper();

        AggregatedHttpMessage response;

        final RequestJsonObj1 obj1 = new RequestJsonObj1(1, "abc");
        final String content1 = mapper.writeValueAsString(obj1);

        response = client.post("/1/convert1", content1).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().toStringUtf8()).isEqualTo(obj1.toString());

        // The order of converters
        final RequestJsonObj1 obj1a = new RequestJsonObj1(2, "abca");
        response = client.post("/1/convert2", content1).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().toStringUtf8()).isEqualTo(obj1a.toString());

        // Multiple @RequestObject annotated parameters
        response = client.post("/1/convert3", content1).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().toStringUtf8()).isEqualTo(HttpMethod.POST.name());
    }

    @Test
    public void testDefaultRequestConverter_bean1() throws Exception {
        final HttpClient client = HttpClient.of(rule.uri("/"));
        final ObjectMapper mapper = new ObjectMapper();

        AggregatedHttpMessage response;

        // test for RequestBean1
        RequestBean1 expectedRequestBean = new RequestBean1();
        expectedRequestBean.userName = "john";
        expectedRequestBean.age = 25;
        expectedRequestBean.gender = MALE;
        expectedRequestBean.permissions = Arrays.asList("permission1", "permission2");
        expectedRequestBean.clientName = "TestClient";
        expectedRequestBean.seqNum = 1234L;
        expectedRequestBean.manager = true;

        String expectedResponseContent = mapper.writeValueAsString(expectedRequestBean);

        // Normal Request: POST + Form Data
        HttpData formData = HttpData.ofAscii("age=25&manager=true&gender=male");
        HttpHeaders reqHeaders = HttpHeaders.of(HttpMethod.POST, "/2/default/bean1/john/1234")
                .set(AsciiString.of("x-user-permission"), "permission1,permission2")
                .set(AsciiString.of("x-client-name"), "TestClient").contentType(MediaType.FORM_DATA);

        response = client.execute(AggregatedHttpMessage.of(reqHeaders, formData)).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().toStringUtf8()).isEqualTo(expectedResponseContent);

        // Normal Request: GET + Query String
        reqHeaders = HttpHeaders.of(HttpMethod.GET, "/2/default/bean1/john/1234?age=25&manager=true&gender=MALE")
                .set(AsciiString.of("x-user-permission"), "permission1,permission2")
                .set(AsciiString.of("x-client-name"), "TestClient");

        response = client.execute(AggregatedHttpMessage.of(reqHeaders)).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().toStringUtf8()).isEqualTo(expectedResponseContent);

        // Bad Request: age=badParam
        reqHeaders = HttpHeaders
                .of(HttpMethod.GET, "/2/default/bean1/john/1234?age=badParam&manager=true&gender=male")
                .set(AsciiString.of("x-user-permission"), "permission1,permission2")
                .set(AsciiString.of("x-client-name"), "TestClient");

        response = client.execute(AggregatedHttpMessage.of(reqHeaders)).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.BAD_REQUEST);

        // Bad Request: seqNum=badParam
        reqHeaders = HttpHeaders
                .of(HttpMethod.GET, "/2/default/bean1/john/badParam?age=25&manager=true&gender=MALE")
                .set(AsciiString.of("x-user-permission"), "permission1,permission2")
                .set(AsciiString.of("x-client-name"), "TestClient");

        response = client.execute(AggregatedHttpMessage.of(reqHeaders)).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.BAD_REQUEST);

        // Bad Request: gender=badParam
        reqHeaders = HttpHeaders
                .of(HttpMethod.GET, "/2/default/bean1/john/1234?age=25&manager=true&gender=badParam")
                .set(AsciiString.of("x-user-permission"), "permission1,permission2")
                .set(AsciiString.of("x-client-name"), "TestClient");

        response = client.execute(AggregatedHttpMessage.of(reqHeaders)).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.BAD_REQUEST);
    }

    @Test
    public void testDefaultRequestConverter_bean2() throws Exception {
        final HttpClient client = HttpClient.of(rule.uri("/"));
        final ObjectMapper mapper = new ObjectMapper();

        AggregatedHttpMessage response;

        // test for RequestBean2
        RequestBean2 expectedRequestBean = new RequestBean2(98765L, "abcd-efgh");
        expectedRequestBean.userName = "john";
        expectedRequestBean.age = 25;
        expectedRequestBean.gender = MALE;
        expectedRequestBean.permissions = Arrays.asList("permission1", "permission2");
        expectedRequestBean.clientName = "TestClient";

        String expectedResponseContent = mapper.writeValueAsString(expectedRequestBean);

        // Normal Request: POST + Form Data
        HttpData formData = HttpData.ofAscii("age=25&gender=male");
        HttpHeaders reqHeaders = HttpHeaders.of(HttpMethod.POST, "/2/default/bean2/john/98765")
                .set(AsciiString.of("x-user-permission"), "permission1,permission2")
                .set(AsciiString.of("x-client-name"), "TestClient").set(AsciiString.of("uid"), "abcd-efgh")
                .contentType(MediaType.FORM_DATA);

        response = client.execute(AggregatedHttpMessage.of(reqHeaders, formData)).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().toStringUtf8()).isEqualTo(expectedResponseContent);

        // Normal Request: GET + Query String
        reqHeaders = HttpHeaders.of(HttpMethod.GET, "/2/default/bean2/john?age=25&gender=MALE&serialNo=98765")
                .set(AsciiString.of("x-user-permission"), "permission1,permission2")
                .set(AsciiString.of("x-client-name"), "TestClient").set(AsciiString.of("uid"), "abcd-efgh");

        response = client.execute(AggregatedHttpMessage.of(reqHeaders)).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().toStringUtf8()).isEqualTo(expectedResponseContent);
    }

    @Test
    public void testDefaultRequestConverter_bean3() throws Exception {
        final HttpClient client = HttpClient.of(rule.uri("/"));
        final ObjectMapper mapper = new ObjectMapper();

        AggregatedHttpMessage response;

        // test for RequestBean3
        RequestBean3 expectedRequestBean = new RequestBean3(3349);
        expectedRequestBean.userName = "john";
        expectedRequestBean.age = 25;
        expectedRequestBean.gender = MALE;
        expectedRequestBean.permissions = Arrays.asList("permission1", "permission2");
        expectedRequestBean.clientName = "TestClient";

        String expectedResponseContent = mapper.writeValueAsString(expectedRequestBean);

        // Normal Request: POST + Form Data
        HttpData formData = HttpData.ofAscii("age=25&gender=male");
        HttpHeaders reqHeaders = HttpHeaders.of(HttpMethod.POST, "/2/default/bean3/john/3349")
                .set(AsciiString.of("x-user-permission"), "permission1,permission2")
                .set(AsciiString.of("x-client-name"), "TestClient").contentType(MediaType.FORM_DATA);

        response = client.execute(AggregatedHttpMessage.of(reqHeaders, formData)).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().toStringUtf8()).isEqualTo(expectedResponseContent);

        // Normal Request: GET + Query String
        reqHeaders = HttpHeaders.of(HttpMethod.GET, "/2/default/bean3/john?age=25&gender=MALE&departmentNo=3349")
                .set(AsciiString.of("x-user-permission"), "permission1,permission2")
                .set(AsciiString.of("x-client-name"), "TestClient");

        response = client.execute(AggregatedHttpMessage.of(reqHeaders)).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().toStringUtf8()).isEqualTo(expectedResponseContent);
    }

    @Test
    public void testDefaultRequestConverter_beanDefineError() throws Exception {
        // error: more than 1 annotated constructors
        assertThatThrownBy(() -> register(BadRequestBeanMoreThanOnConstructor01.class))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("too many annotated constructors");
        assertThatThrownBy(() -> register(BadRequestBeanMoreThanOnConstructor02.class))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("too many annotated constructors");
        assertThatThrownBy(() -> register(BadRequestBeanMoreThanOnConstructor03.class))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("too many annotated constructors");

        // error: annotation used in constructor param
        assertThatThrownBy(() -> register(BadRequestBeanAnnotationInConstructorParam.class))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("no @Param and @Header annotations are allowed");

        // error: annotation used in method param
        assertThatThrownBy(() -> register(BadRequestBeanAnnotationInMethodParam.class))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("no @Param and @Header annotations are allowed");

        // error: more than one params for annotated constructor
        assertThatThrownBy(() -> register(BadRequestBeanMoreThanOneConstructorParam.class))
                .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("the number of parameters")
                .hasMessageContaining("(expected: 1)");

        // error: more than one params for annotated method
        assertThatThrownBy(() -> register(BadRequestBeanMoreThanOneMethodParam.class))
                .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("the number of parameters")
                .hasMessageContaining("(expected: 1)");

        // error: some constructor params not annotated
        assertThatThrownBy(() -> register(BadRequestBeanSomeConstructorParamWithoutAnnotation.class))
                .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("every parameter of")
                .hasMessageContaining("should be annotated with one of");

        // error: some method params not annotated
        assertThatThrownBy(() -> register(BadRequestBeanSomeMethodParamWithoutAnnotation.class))
                .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("every parameter of")
                .hasMessageContaining("should be annotated with one of");
    }

    @Test
    public void testDefaultRequestConverter_json() throws Exception {
        final HttpClient client = HttpClient.of(rule.uri("/"));
        final ObjectMapper mapper = new ObjectMapper();

        AggregatedHttpMessage response;

        final RequestJsonObj1 obj1 = new RequestJsonObj1(1, "abc");
        final String content1 = mapper.writeValueAsString(obj1);

        // MediaType.JSON_UTF_8
        response = client.execute(
                AggregatedHttpMessage.of(HttpMethod.POST, "/2/default/json", MediaType.JSON_UTF_8, content1))
                .aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().toStringUtf8()).isEqualTo("abc");

        // MediaType.JSON_PATCH
        // obj1 is not a json-patch+json format, but just check if it's converted by
        // DefaultRequestConverter when it is valid JSON format
        response = client.execute(
                AggregatedHttpMessage.of(HttpMethod.POST, "/2/default/json", MediaType.JSON_PATCH, content1))
                .aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().toStringUtf8()).isEqualTo("abc");

        // "application/vnd.api+json"
        response = client.execute(AggregatedHttpMessage.of(HttpMethod.POST, "/2/default/json",
                MediaType.create("application", "vnd.api+json"), content1)).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().toStringUtf8()).isEqualTo("abc");

        String invalidJson = "{\"foo:\"bar\"}"; // should be \"foo\"
        response = client.execute(AggregatedHttpMessage.of(HttpMethod.POST, "/2/default/invalidJson",
                MediaType.JSON_UTF_8, invalidJson)).aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.BAD_REQUEST);
    }

    @Test
    public void testDefaultRequestConverter_binary() throws Exception {
        final HttpClient client = HttpClient.of(rule.uri("/"));

        AggregatedHttpMessage response;

        final byte[] binary = { 0x00, 0x01, 0x02 };
        response = client.execute(
                AggregatedHttpMessage.of(HttpMethod.POST, "/2/default/binary", MediaType.OCTET_STREAM, binary))
                .aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().array()).isEqualTo(binary);
    }

    @Test
    public void testDefaultRequestConverter_text() throws Exception {
        final HttpClient client = HttpClient.of(rule.uri("/"));

        AggregatedHttpMessage response;

        final byte[] utf8 = "".getBytes(StandardCharsets.UTF_8);
        response = client.execute(
                AggregatedHttpMessage.of(HttpMethod.POST, "/2/default/text", MediaType.PLAIN_TEXT_UTF_8, utf8))
                .aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        assertThat(response.content().array()).isEqualTo(utf8);

        final MediaType textPlain = MediaType.create("text", "plain");
        final byte[] iso8859_1 = "".getBytes(StandardCharsets.ISO_8859_1);
        response = client
                .execute(AggregatedHttpMessage.of(HttpMethod.POST, "/2/default/text", textPlain, iso8859_1))
                .aggregate().join();
        assertThat(response.headers().status()).isEqualTo(HttpStatus.OK);
        // Response is encoded as UTF-8.
        assertThat(response.content().array()).isEqualTo(utf8);
    }
}