Search code examples
springspring-bootintegration-testingspring-boot-testmockmvc

MockMvc with SpringBootTest is throwing exception HttpMediaTypeNotSupportedException when testing the controller


I am testing request validation in Spring RestController with the help of integration testing and MockMvc.

ControllerTest.java

@ExtendWith(MockitoExtension.class)
@SpringBootTest(classes = Controller.class)
@AutoConfigureMockMvc(addFilters = false)
class ControllerTest {

    private ObjectMapper objectMapper;

    @MockBean
    private Service service;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        objectMapper = new ObjectMapper();
    }

    @Test
    void createOrAdd_shouldReturnErrorResponseOnInvalidInput() throws Exception {
        Request request = Request.builder()
                .name("name<script>")
                .primaryEmail("[email protected]")
                .build();

        mockMvc.perform(MockMvcRequestBuilders.post("/api/create")
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON_VALUE)
                        .content(objectMapper.writeValueAsString(request))
                .characterEncoding("utf-8"))
                .andExpect(MockMvcResultMatchers.status().isBadRequest());
    }
}

Controller.java :

@Slf4j
@RestController
public class Controller {

    private final Service service;

    public Controller(Service service) {
        this.service = service;
    }

    @PostMapping(value = "/api/create", consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<GenericResponse<Response>> createOrAdd(@RequestBody @Valid Request request, Errors errors) {
        GenericResponse<Response> genericResponse = new GenericResponse<>();
        try {
            if (errors.hasErrors()) {
                throw new RequestParamsException(errors.getAllErrors());
            }
            Response response = service.createOrAdd(request);
            genericResponse.setData(response);
            return ResponseEntity.ok().body(genericResponse);
        } catch (RequestParamsException ex) {
            genericResponse.setErrors(ex.getErrors());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(genericResponse);
        }
    }

Error :

WARN 17304 --- [           main] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/json;charset=utf-8' not supported]

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /api/create
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=utf-8", Accept:"application/json", Content-Length:"162"]
             Body = {"name":"name<script>alert(1)</script>","primary_email_address":"[email protected]"}
    Session Attrs = {}

Handler:
             Type = com.org.controller.Controller
           Method = com.org.controller.Controller#createOrAdd(Request, Errors)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = org.springframework.web.HttpMediaTypeNotSupportedException

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 415
    Error message = null
          Headers = [Accept:"application/octet-stream, text/plain, application/xml, text/xml, application/x-www-form-urlencoded, application/*+xml, multipart/form-data, multipart/mixed, */*"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

java.lang.AssertionError: Status expected:<400> but was:<415>
Expected :400
Actual   :415

I have used the correct Content-Type and Accept Headers while making a call in Test.java using mockMvc, but still it's giving HttpMediaTypeNotSupportedException. Tried many combinations in Accept and Content-Type but still not working.

I have read many SO questions related to this exception, but couldn't find what's the issue here. Still not able to figure out why it's saying HttpMediaTypeNotSupportedException.

Update : After removing addFilters = false as suggested, not able to find the handler itself.

    MockHttpServletRequest:
          HTTP Method = POST
          Request URI = /api/create
           Parameters = {}
              Headers = [Content-Type:"application/json;charset=utf-8", Accept:"application/json", Content-Length:"162"]
                 Body = {"name":"name<script>alert(1)</script>","primary_email_address":"[email protected]"}
    Session Attrs = {org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN=org.springframework.security.web.csrf.DefaultCsrfToken@7a687d8d}   

     
Handler:
             Type = null

Async:
    Async started = false
     Async result = null
    
    Resolved Exception:
             Type = null
    
    ModelAndView:
            View name = null
                 View = null
                Model = null
    
    FlashMap:
           Attributes = null
    
MockHttpServletResponse:
           Status = 403
    Error message = Forbidden
          Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

    
java.lang.AssertionError: Status expected:<400> but was:<403>
Expected :400
Actual   :403

Request :

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CreateAgencyRequest {

    @NotNull(message = "name can't be null")
    @JsonProperty(value = "name")
    @Pattern(regexp = REGEX_CONST, message = "name is not valid")
    private String name;

    @NotNull(message = "primary_email_address can't be null")
    @JsonProperty(value = "primary_email_address")
    private String primaryEmail;

}

Solution

  • Lets take a look at your test.

    @WebMvcTest(classes = Controller.class)
    @AutoConfigureMockMvc(addFilters = false)
    class ControllerTest {
    
        private ObjectMapper objectMapper;
    
        @MockBean
        private Service service;
    
        @Autowired
        private MockMvc mockMvc;
    
        @Test
        void createOrAdd_shouldReturnErrorResponseOnInvalidInput() throws Exception {
            Request request = Request.builder()
                    .name("name<script>")
                    .primaryEmail("[email protected]")
                    .build();
    
            mockMvc.perform(MockMvcRequestBuilders.post("/api/create")
                            .accept(MediaType.APPLICATION_JSON)
                            .contentType(MediaType.APPLICATION_JSON_VALUE)
                            .content(objectMapper.writeValueAsString(request))
                    .characterEncoding("utf-8"))
                    .andExpect(MockMvcResultMatchers.status().isBadRequest());
        }
    }
    

    First the @ExtendWith(MockitoExtension.class) doesn't add anything as you are using, correctly, the @MockBean annotation which is handled by Spring. So you should remove that.

    Next the @SpringBootTest is for bootstrapping the full application to run an integration test. What you want is a sliced test for the web, so instead of @SpringBootTest use @WebMvcTest this will make your test considerably faster. You can then also remove @AutoConfigureMockMvc as that is added by default.

    You disabled all filters with @AutoConfigureMockMvc(addFilters = false) probably due to the 403 you got. The 403 you have is the result of enabling CSRF (enabled by default) and not adding that to the request. If you don't want CSRF (you probably want it) either disable that in the Security configuration, or if you want it modify your request.

    Looking at the error you have the culprit is the characterEncoding being added to the content type, so you probably want/should remove that.

    With all that your test should look something like this.

    
    import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
    
    @WebMvcTest(Controller.class)
    class ControllerTest {
    
        private ObjectMapper objectMapper = new ObjectMapper();
    
        @MockBean
        private Service service;
    
        @Autowired
        private MockMvc mockMvc;
    
        @Test
        void createOrAdd_shouldReturnErrorResponseOnInvalidInput() throws Exception {
            Request request = Request.builder()
                    .name("name<script>")
                    .primaryEmail("[email protected]")
                    .build();
    
            mockMvc.perform(MockMvcRequestBuilders.post("/api/create")
                            .with(csrf()) // Add CSRF field for security
                            .accept(MediaType.APPLICATION_JSON)
                            .contentType(MediaType.APPLICATION_JSON_VALUE)
                            .content(objectMapper.writeValueAsString(request))
                    .andExpect(MockMvcResultMatchers.status().isBadRequest());
        }
    }
    

    NOTE: If you also have authentication in place you might also need to add a user/password to the request as explained here