View Javadoc
1   /*
2    * Copyright 2020 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.bremersee.spring.web.reactive.multipart;
18  
19  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertFalse;
22  import static org.junit.jupiter.api.Assertions.assertNotNull;
23  import static org.junit.jupiter.api.Assertions.assertNull;
24  import static org.junit.jupiter.api.Assertions.assertTrue;
25  import static org.mockito.ArgumentMatchers.any;
26  import static org.mockito.Mockito.mock;
27  import static org.mockito.Mockito.when;
28  
29  import eu.maxschuster.dataurl.DataUrl;
30  import eu.maxschuster.dataurl.DataUrlBuilder;
31  import eu.maxschuster.dataurl.DataUrlEncoding;
32  import eu.maxschuster.dataurl.DataUrlSerializer;
33  import java.io.File;
34  import java.io.IOException;
35  import java.net.MalformedURLException;
36  import java.nio.charset.StandardCharsets;
37  import java.nio.file.Files;
38  import java.nio.file.Path;
39  import java.nio.file.StandardOpenOption;
40  import java.util.List;
41  import java.util.UUID;
42  import org.bremersee.spring.web.multipart.FileAwareMultipartFile;
43  import org.junit.jupiter.api.Test;
44  import org.springframework.http.HttpHeaders;
45  import org.springframework.http.MediaType;
46  import org.springframework.http.codec.multipart.FilePart;
47  import org.springframework.http.codec.multipart.FormFieldPart;
48  import org.springframework.http.codec.multipart.Part;
49  import org.springframework.util.LinkedMultiValueMap;
50  import org.springframework.util.MultiValueMap;
51  import org.springframework.web.multipart.MultipartException;
52  import org.springframework.web.multipart.MultipartFile;
53  import reactor.core.publisher.Flux;
54  import reactor.core.publisher.Mono;
55  import reactor.test.StepVerifier;
56  
57  /**
58   * The multipart file builder implementation test.
59   *
60   * @author Christian Bremer
61   */
62  class MultipartFileBuilderImplTest {
63  
64    /**
65     * Build with null.
66     */
67    @Test
68    void buildWithNull() {
69      MultipartFileBuilder builder = new MultipartFileBuilderImpl();
70      StepVerifier
71          .create(builder.build((Part) null))
72          .assertNext(multipartFile -> {
73            assertTrue(multipartFile.isEmpty());
74            assertNull(multipartFile.getContentType());
75            assertNull(multipartFile.getOriginalFilename());
76            assertEquals(0L, multipartFile.getSize());
77          })
78          .verifyComplete();
79    }
80  
81    /**
82     * Build with file part.
83     */
84    @Test
85    void buildWithFilePart() {
86      MultipartFileBuilder builder = new MultipartFileBuilderImpl(
87          System.getProperty("java.io.tmpdir"));
88      final byte[] value = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
89      StepVerifier
90          .create(builder.build(createFilePart(value, "file", MediaType.TEXT_PLAIN, "test.txt")))
91          .assertNext(multipartFile -> {
92            try {
93              assertFalse(multipartFile.isEmpty());
94              assertEquals("file", multipartFile.getName());
95              assertEquals(MediaType.TEXT_PLAIN_VALUE, multipartFile.getContentType());
96              assertEquals("test.txt", multipartFile.getOriginalFilename());
97              assertEquals(value.length, (int) multipartFile.getSize());
98              assertArrayEquals(value, multipartFile.getBytes());
99  
100           } catch (IOException e) {
101             throw new MultipartException("Fatal error", e);
102           } finally {
103             FileAwareMultipartFile.delete(multipartFile);
104           }
105         })
106         .verifyComplete();
107   }
108 
109   /**
110    * Build with form field part.
111    *
112    * @throws MalformedURLException the malformed url exception
113    */
114   @Test
115   void buildWithFormFieldPart() throws MalformedURLException {
116     MultipartFileBuilder builder = new MultipartFileBuilderImpl(
117         new File(System.getProperty("java.io.tmpdir")));
118     final byte[] value = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
119     DataUrl dataUrl = new DataUrlBuilder()
120         .setData(value)
121         .setCharset(StandardCharsets.UTF_8.name())
122         .setEncoding(DataUrlEncoding.BASE64)
123         .setMimeType(MediaType.IMAGE_PNG_VALUE)
124         .build();
125     StepVerifier
126         .create(
127             builder.build(createFormFieldPart(new DataUrlSerializer().serialize(dataUrl), "file")))
128         .assertNext(multipartFile -> {
129           try {
130             assertFalse(multipartFile.isEmpty());
131             assertEquals("file", multipartFile.getName());
132             assertEquals(MediaType.IMAGE_PNG_VALUE, multipartFile.getContentType());
133             assertEquals(value.length, (int) multipartFile.getSize());
134             assertArrayEquals(value, multipartFile.getBytes());
135           } catch (IOException e) {
136             throw new MultipartException("Fatal error", e);
137           } finally {
138             FileAwareMultipartFile.delete(multipartFile);
139           }
140         })
141         .verifyComplete();
142   }
143 
144   /**
145    * Build from flux.
146    */
147   @Test
148   void buildFromFlux() {
149     MultipartFileBuilder builder = new MultipartFileBuilderImpl();
150     final byte[] value = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
151     StepVerifier.create(builder
152             .build(Flux.just(createFilePart(value, "file", MediaType.TEXT_PLAIN, "test.txt"))))
153         .assertNext(multipartFile -> {
154           try {
155             assertFalse(multipartFile.isEmpty());
156             assertEquals("file", multipartFile.getName());
157             assertEquals(MediaType.TEXT_PLAIN_VALUE, multipartFile.getContentType());
158             assertEquals("test.txt", multipartFile.getOriginalFilename());
159             assertEquals(value.length, (int) multipartFile.getSize());
160             assertArrayEquals(value, multipartFile.getBytes());
161 
162           } catch (IOException e) {
163             throw new MultipartException("Fatal error", e);
164           } finally {
165             FileAwareMultipartFile.delete(multipartFile);
166           }
167         })
168         .verifyComplete();
169 
170     StepVerifier.create(builder.build(Flux.empty()))
171         .assertNext(multipartFile -> assertTrue(multipartFile.isEmpty()))
172         .verifyComplete();
173   }
174 
175   /**
176    * Build list from flux.
177    *
178    * @throws IOException the io exception
179    */
180   @Test
181   void buildListFromFlux() throws IOException {
182     final byte[] content0 = "image-content".getBytes(StandardCharsets.UTF_8);
183     Part part1 = createFilePart(content0, "part1", MediaType.IMAGE_JPEG, "img.jpg");
184     final byte[] content1 = "text".getBytes(StandardCharsets.UTF_8);
185     DataUrl dataUrl = new DataUrlBuilder()
186         .setData(content1)
187         .setCharset(StandardCharsets.UTF_8.name())
188         .setEncoding(DataUrlEncoding.BASE64)
189         .setMimeType("text/plain")
190         .build();
191     Part part2 = createFormFieldPart(new DataUrlSerializer().serialize(dataUrl), "part2");
192 
193     MultipartFileBuilder builder = new MultipartFileBuilderImpl();
194     StepVerifier.create(builder
195             .buildList(Flux.just(part1, part2)))
196         .assertNext(multipartFiles -> {
197           try {
198             assertEquals(2, multipartFiles.size());
199 
200             MultipartFile obj0 = MultipartFileBuilder.getMultipartFile(multipartFiles, 0);
201             assertNotNull(obj0);
202             assertEquals("part1", obj0.getName());
203             assertEquals(MediaType.IMAGE_JPEG_VALUE, obj0.getContentType());
204             assertEquals(content0.length, (int) obj0.getSize());
205             assertArrayEquals(content0, obj0.getBytes());
206 
207             MultipartFile obj1 = MultipartFileBuilder.getMultipartFile(multipartFiles, 1);
208             assertNotNull(obj1);
209             assertEquals("part2", obj1.getName());
210             assertEquals(MediaType.TEXT_PLAIN_VALUE, obj1.getContentType());
211             assertEquals(content1.length, (int) obj1.getSize());
212             assertArrayEquals(content1, obj1.getBytes());
213 
214           } catch (IOException e) {
215             throw new MultipartException("Internal error", e);
216           } finally {
217             multipartFiles.forEach(FileAwareMultipartFile::delete);
218           }
219         })
220         .verifyComplete();
221   }
222 
223   /**
224    * Build map from flux array.
225    *
226    * @throws IOException the io exception
227    */
228   @Test
229   void buildMapFromFluxArray() throws IOException {
230     final byte[] content0 = "image-content".getBytes(StandardCharsets.UTF_8);
231     Part part1 = createFilePart(content0, "part1", MediaType.IMAGE_JPEG, "img.jpg");
232     final byte[] content1 = "text".getBytes(StandardCharsets.UTF_8);
233     DataUrl dataUrl = new DataUrlBuilder()
234         .setData(content1)
235         .setCharset(StandardCharsets.UTF_8.name())
236         .setEncoding(DataUrlEncoding.BASE64)
237         .setMimeType("text/plain")
238         .build();
239     Part part2 = createFormFieldPart(new DataUrlSerializer().serialize(dataUrl), "part2");
240 
241     MultipartFileBuilder builder = new MultipartFileBuilderImpl();
242     //noinspection unchecked
243     StepVerifier.create(builder.buildMap(Flux.just(part1), Flux.just(part2)))
244         .assertNext(multipartFileMap -> {
245           try {
246             assertEquals(2, multipartFileMap.size());
247 
248             MultipartFile obj0 = MultipartFileBuilder.getMultipartFile(multipartFileMap, "part1");
249             assertNotNull(obj0);
250             assertEquals(MediaType.IMAGE_JPEG_VALUE, obj0.getContentType());
251             assertEquals(content0.length, (int) obj0.getSize());
252             assertArrayEquals(content0, obj0.getBytes());
253 
254             MultipartFile obj1 = MultipartFileBuilder.getMultipartFile(multipartFileMap, "part2");
255             assertNotNull(obj1);
256             assertEquals(MediaType.TEXT_PLAIN_VALUE, obj1.getContentType());
257             assertEquals(content1.length, (int) obj1.getSize());
258             assertArrayEquals(content1, obj1.getBytes());
259 
260           } catch (IOException e) {
261             throw new MultipartException("Internal error", e);
262           } finally {
263             multipartFileMap.values().forEach(FileAwareMultipartFile::delete);
264           }
265         })
266         .verifyComplete();
267   }
268 
269   /**
270    * Build multi value map from flux array.
271    *
272    * @throws IOException the io exception
273    */
274   @Test
275   void buildMultiValueMapFromFluxArray() throws IOException {
276     final byte[] content0 = "image-content".getBytes(StandardCharsets.UTF_8);
277     Part part1 = createFilePart(content0, "part1", MediaType.IMAGE_JPEG, "img.jpg");
278     final byte[] content1 = "text".getBytes(StandardCharsets.UTF_8);
279     DataUrl dataUrl = new DataUrlBuilder()
280         .setData(content1)
281         .setCharset(StandardCharsets.UTF_8.name())
282         .setEncoding(DataUrlEncoding.BASE64)
283         .setMimeType("text/plain")
284         .build();
285     Part part2 = createFormFieldPart(new DataUrlSerializer().serialize(dataUrl), "part2");
286 
287     MultipartFileBuilder builder = new MultipartFileBuilderImpl();
288     //noinspection unchecked
289     StepVerifier.create(builder.buildMultiValueMap(Flux.just(part1), Flux.just(part2)))
290         .assertNext(multipartFileMap -> {
291           try {
292             assertEquals(2, multipartFileMap.size());
293 
294             MultipartFile obj0 = MultipartFileBuilder
295                 .getFirstMultipartFile(multipartFileMap, "part1");
296             assertNotNull(obj0);
297             assertEquals(MediaType.IMAGE_JPEG_VALUE, obj0.getContentType());
298             assertEquals(content0.length, (int) obj0.getSize());
299             assertArrayEquals(content0, obj0.getBytes());
300 
301             MultipartFile obj1 = MultipartFileBuilder
302                 .getFirstMultipartFile(multipartFileMap, "part2");
303             assertNotNull(obj1);
304             assertEquals(MediaType.TEXT_PLAIN_VALUE, obj1.getContentType());
305             assertEquals(content1.length, (int) obj1.getSize());
306             assertArrayEquals(content1, obj1.getBytes());
307 
308           } catch (IOException e) {
309             throw new MultipartException("Internal error", e);
310           } finally {
311             multipartFileMap.values()
312                 .forEach(multipartFiles -> multipartFiles.forEach(FileAwareMultipartFile::delete));
313           }
314         })
315         .verifyComplete();
316   }
317 
318   /**
319    * Build list.
320    *
321    * @throws IOException the io exception
322    */
323   @Test
324   void buildList() throws IOException {
325     MultiValueMap<String, Part> multiPartData = new LinkedMultiValueMap<>();
326     final byte[] content0 = "image-content".getBytes(StandardCharsets.UTF_8);
327     multiPartData.set(
328         "part1",
329         createFilePart(content0, "part1", MediaType.IMAGE_JPEG, "img.jpg"));
330     final byte[] content1 = "text".getBytes(StandardCharsets.UTF_8);
331     DataUrl dataUrl = new DataUrlBuilder()
332         .setData(content1)
333         .setCharset(StandardCharsets.UTF_8.name())
334         .setEncoding(DataUrlEncoding.BASE64)
335         .setMimeType("text/plain")
336         .build();
337     multiPartData.set(
338         "part2",
339         createFormFieldPart(new DataUrlSerializer().serialize(dataUrl), "part2"));
340 
341     MultipartFileBuilder builder = new MultipartFileBuilderImpl();
342     StepVerifier.create(builder
343             .buildList(multiPartData, "part1", "part2"))
344         .assertNext(multipartFiles -> {
345           try {
346             assertEquals(2, multipartFiles.size());
347 
348             MultipartFile obj0 = MultipartFileBuilder.getMultipartFile(multipartFiles, 0);
349             assertNotNull(obj0);
350             assertEquals("part1", obj0.getName());
351             assertEquals(MediaType.IMAGE_JPEG_VALUE, obj0.getContentType());
352             assertEquals(content0.length, (int) obj0.getSize());
353             assertArrayEquals(content0, obj0.getBytes());
354 
355             MultipartFile obj1 = MultipartFileBuilder.getMultipartFile(multipartFiles, 1);
356             assertNotNull(obj1);
357             assertEquals("part2", obj1.getName());
358             assertEquals(MediaType.TEXT_PLAIN_VALUE, obj1.getContentType());
359             assertEquals(content1.length, (int) obj1.getSize());
360             assertArrayEquals(content1, obj1.getBytes());
361 
362           } catch (IOException e) {
363             throw new MultipartException("Internal error", e);
364           } finally {
365             multipartFiles.forEach(FileAwareMultipartFile::delete);
366           }
367         })
368         .verifyComplete();
369   }
370 
371   /**
372    * Build lists.
373    *
374    * @throws IOException the io exception
375    */
376   @Test
377   void buildLists() throws IOException {
378     MultiValueMap<String, Part> multiPartData = new LinkedMultiValueMap<>();
379     final byte[] content0 = "image-content".getBytes(StandardCharsets.UTF_8);
380     multiPartData.add(
381         "part",
382         createFilePart(content0, "part", MediaType.IMAGE_JPEG, "img.jpg"));
383     final byte[] content1 = "text".getBytes(StandardCharsets.UTF_8);
384     DataUrl dataUrl = new DataUrlBuilder()
385         .setData(content1)
386         .setCharset(StandardCharsets.UTF_8.name())
387         .setEncoding(DataUrlEncoding.BASE64)
388         .setMimeType("text/plain")
389         .build();
390     multiPartData.add(
391         "part",
392         createFormFieldPart(new DataUrlSerializer().serialize(dataUrl), "part"));
393 
394     MultipartFileBuilder builder = new MultipartFileBuilderImpl();
395     StepVerifier
396         .create(builder.buildLists(multiPartData, "part"))
397         .assertNext(multipartFiles -> {
398           try {
399             assertEquals(2, multipartFiles.size());
400 
401             MultipartFile obj0 = MultipartFileBuilder.getMultipartFile(multipartFiles, 0);
402             assertNotNull(obj0);
403             assertEquals("part", obj0.getName());
404             assertEquals(MediaType.IMAGE_JPEG_VALUE, obj0.getContentType());
405             assertEquals(content0.length, (int) obj0.getSize());
406             assertArrayEquals(content0, obj0.getBytes());
407 
408             MultipartFile obj1 = MultipartFileBuilder.getMultipartFile(multipartFiles, 1);
409             assertNotNull(obj1);
410             assertEquals("part", obj1.getName());
411             assertEquals(MediaType.TEXT_PLAIN_VALUE, obj1.getContentType());
412             assertEquals(content1.length, (int) obj1.getSize());
413             assertArrayEquals(content1, obj1.getBytes());
414 
415           } catch (IOException e) {
416             throw new MultipartException("Internal error", e);
417           } finally {
418             multipartFiles.forEach(FileAwareMultipartFile::delete);
419           }
420         })
421         .verifyComplete();
422   }
423 
424   /**
425    * Build map.
426    *
427    * @throws IOException the io exception
428    */
429   @Test
430   void buildMap() throws IOException {
431     MultiValueMap<String, Part> multiPartData = new LinkedMultiValueMap<>();
432     final byte[] content0 = "image-content".getBytes(StandardCharsets.UTF_8);
433     multiPartData.set(
434         "part1",
435         createFilePart(content0, "part1", MediaType.IMAGE_JPEG, "img.jpg"));
436     final byte[] content1 = "text".getBytes(StandardCharsets.UTF_8);
437     DataUrl dataUrl = new DataUrlBuilder()
438         .setData(content1)
439         .setCharset(StandardCharsets.UTF_8.name())
440         .setEncoding(DataUrlEncoding.BASE64)
441         .setMimeType("text/plain")
442         .build();
443     multiPartData.set(
444         "part2",
445         createFormFieldPart(new DataUrlSerializer().serialize(dataUrl), "part2"));
446 
447     MultipartFileBuilder builder = new MultipartFileBuilderImpl();
448     StepVerifier.create(builder
449             .buildMap(multiPartData, "part1", "part2"))
450         .assertNext(multipartFileMap -> {
451           try {
452             assertEquals(2, multipartFileMap.size());
453 
454             MultipartFile obj0 = MultipartFileBuilder.getMultipartFile(multipartFileMap, "part1");
455             assertNotNull(obj0);
456             assertEquals(MediaType.IMAGE_JPEG_VALUE, obj0.getContentType());
457             assertEquals(content0.length, (int) obj0.getSize());
458             assertArrayEquals(content0, obj0.getBytes());
459 
460             MultipartFile obj1 = MultipartFileBuilder.getMultipartFile(multipartFileMap, "part2");
461             assertNotNull(obj1);
462             assertEquals(MediaType.TEXT_PLAIN_VALUE, obj1.getContentType());
463             assertEquals(content1.length, (int) obj1.getSize());
464             assertArrayEquals(content1, obj1.getBytes());
465 
466           } catch (IOException e) {
467             throw new MultipartException("Internal error", e);
468           } finally {
469             multipartFileMap.values().forEach(FileAwareMultipartFile::delete);
470           }
471         })
472         .verifyComplete();
473   }
474 
475   /**
476    * Build multi value map.
477    *
478    * @throws IOException the io exception
479    */
480   @Test
481   void buildMultiValueMap() throws IOException {
482     MultiValueMap<String, Part> multiPartData = new LinkedMultiValueMap<>();
483     final byte[] content0 = "image-content".getBytes(StandardCharsets.UTF_8);
484     multiPartData.add(
485         "part",
486         createFilePart(content0, "part", MediaType.IMAGE_JPEG, "img.jpg"));
487     final byte[] content1 = "text".getBytes(StandardCharsets.UTF_8);
488     DataUrl dataUrl = new DataUrlBuilder()
489         .setData(content1)
490         .setCharset(StandardCharsets.UTF_8.name())
491         .setEncoding(DataUrlEncoding.BASE64)
492         .setMimeType("text/plain")
493         .build();
494     multiPartData.add(
495         "part",
496         createFormFieldPart(new DataUrlSerializer().serialize(dataUrl), "part"));
497 
498     MultipartFileBuilder builder = new MultipartFileBuilderImpl();
499     StepVerifier
500         .create(builder.buildMultiValueMap(multiPartData, "part"))
501         .assertNext(map -> {
502           List<MultipartFile> multipartFiles = MultipartFileBuilder.getMultipartFiles(map, "part");
503           assertFalse(multipartFiles.isEmpty());
504           try {
505             assertEquals(2, multipartFiles.size());
506 
507             MultipartFile obj0 = MultipartFileBuilder.getMultipartFile(multipartFiles, 0);
508             assertNotNull(obj0);
509             assertEquals("part", obj0.getName());
510             assertEquals(MediaType.IMAGE_JPEG_VALUE, obj0.getContentType());
511             assertEquals(content0.length, (int) obj0.getSize());
512             assertArrayEquals(content0, obj0.getBytes());
513 
514             MultipartFile obj1 = MultipartFileBuilder.getMultipartFile(multipartFiles, 1);
515             assertNotNull(obj1);
516             assertEquals("part", obj1.getName());
517             assertEquals(MediaType.TEXT_PLAIN_VALUE, obj1.getContentType());
518             assertEquals(content1.length, (int) obj1.getSize());
519             assertArrayEquals(content1, obj1.getBytes());
520 
521           } catch (IOException e) {
522             throw new MultipartException("Internal error", e);
523           } finally {
524             multipartFiles.forEach(FileAwareMultipartFile::delete);
525           }
526         })
527         .verifyComplete();
528   }
529 
530   private FilePart createFilePart(byte[] content, String parameterName, MediaType contentType,
531       String filename) {
532     FilePart part = mock(FilePart.class);
533     when(part.name()).thenReturn(parameterName);
534     when(part.filename()).thenReturn(filename);
535     when(part.transferTo(any(File.class))).then(invocationOnMock -> {
536       try {
537         File file = invocationOnMock.getArgument(0);
538         file.deleteOnExit();
539         Files.write(
540             file.toPath(),
541             content,
542             StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
543       } catch (Exception e) {
544         return Mono.error(e);
545       }
546       return Mono.empty();
547     });
548     when(part.transferTo(any(Path.class))).then(invocationOnMock -> {
549       try {
550         Path file = invocationOnMock.getArgument(0);
551         file.toFile().deleteOnExit();
552         Files.write(
553             file,
554             content,
555             StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
556       } catch (Exception e) {
557         return Mono.error(e);
558       }
559       return Mono.empty();
560     });
561     HttpHeaders httpHeaders = new HttpHeaders();
562     httpHeaders.setContentType(contentType);
563     when(part.headers()).thenReturn(httpHeaders);
564     return part;
565   }
566 
567   private FormFieldPart createFormFieldPart(String content, String parameterName) {
568     FormFieldPart part = mock(FormFieldPart.class);
569     when(part.name()).thenReturn(parameterName);
570     when(part.value()).thenReturn(content);
571     return part;
572   }
573 
574 }