javaspringunit-testingspring-testmockmvc

Spring / MockMvc: A multipart post request of file&non-file parts causing issues matching the correct object type


If I want a post request definition like so:

    @PostMapping(path = "/metadata/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    ResponseEntity<Void> test(HttpServletResponse response,//
                              @RequestPart(value = "file") MultipartFile file,
                              @RequestPart(value = "partName") Metadata partName,
                              @PathVariable(name = "id") String artifactId) {
        return null;
    }

(where Metadata is a simple Pojo) and then use the following test:

    @Test
    public void testMetadata() throws Exception {
        mockMvc
                .perform(multipart(HttpMethod.POST, "/metadata/123")
                                 .file("file", "file content".getBytes())
                                 .part(getJsonPart())
                                 .contentType(MediaType.MULTIPART_FORM_DATA_VALUE))
                .andExpect(status().isOk());
    }

    private @NotNull MockPart getJsonPart() throws JsonProcessingException {
        byte[] partContent = new ObjectMapper().writeValueAsBytes(new Metadata("foo"));
        MockPart part = new MockPart("partName", partContent);
        part.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        return part;
    }

It does not match - I get a 400 response instead of a 200. I think it's failing to deserialize the payload.

However, when I change my postmapping's part type from 'Metadata' to 'Object', it works (deserializing the part as a map).

Now I am probably being blind to something obvious, but am unable to resolve this on my own apparently. Any hints? Just to reiterate: what I am trying to get is a controller that has the proper deserialized 'Metadata' object.


Just for completeness, here are the 2 full files:

class Metadata {

    private String value;

    public Metadata(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

@RestController
public class TestController {

    @PostMapping(path = "/metadata/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    ResponseEntity<Void> test(HttpServletResponse response,//
                              @RequestPart(value = "file") MultipartFile file,
                              @RequestPart(value = "partName") Metadata partName,
                              @PathVariable(name = "id") String artifactId) {
        return null;
    }

    @PostMapping(path = "/object/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    ResponseEntity<Void> test(HttpServletResponse response, @RequestPart(value = "file") MultipartFile file,
                              @RequestPart(value = "partName") Object partName,
                              @PathVariable(name = "id") String artifactId) {
        return null;
    }
}
@WebMvcTest(TestController.class)
public class TestControllerTest {

    @Autowired
    public MockMvc mockMvc;

    @Test
    public void failingTest() throws Exception {
        mockMvc
                .perform(multipart(HttpMethod.POST, "/metadata/123")
                                 .file("file", "file content".getBytes())
                                 .part(getJsonPart())
                                 .contentType(MediaType.MULTIPART_FORM_DATA_VALUE))
                .andExpect(status().isOk());
    }

    @Test
    public void passingTest() throws Exception {
        mockMvc
                .perform(multipart(HttpMethod.POST, "/object/123")
                                 .file("file", "file content".getBytes())
                                 .part(getJsonPart())
                                 .contentType(MediaType.MULTIPART_FORM_DATA_VALUE))
                .andExpect(status().isOk());
    }

    private @NotNull MockPart getJsonPart() throws JsonProcessingException {
        byte[] partContent = new ObjectMapper().writeValueAsBytes(new Metadata("foo"));
        MockPart part = new MockPart("partName", partContent);
        part.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        return part;
    }
}

Solution

  • Well, that was embarrasing...

    The problem is the lack of a no-arg constructor, which causes deserialization to fail. But in this scenario, the error message of the deserializer gets lost and doesn't surface.

    So, the solution is simply an empty constructor.