diff --git a/common/src/main/java/de/ozgcloud/eingang/common/zip/ReadZipException.java b/semantik-adapter/src/main/java/de/ozgcloud/eingang/semantik/common/ReadZipException.java similarity index 82% rename from common/src/main/java/de/ozgcloud/eingang/common/zip/ReadZipException.java rename to semantik-adapter/src/main/java/de/ozgcloud/eingang/semantik/common/ReadZipException.java index 2a50bc6bc844a3d0a3f2aec157d1e93f1004f338..c3fcc85a910df48282884211d68617f4095248df 100644 --- a/common/src/main/java/de/ozgcloud/eingang/common/zip/ReadZipException.java +++ b/semantik-adapter/src/main/java/de/ozgcloud/eingang/semantik/common/ReadZipException.java @@ -1,4 +1,4 @@ -package de.ozgcloud.eingang.common.zip; +package de.ozgcloud.eingang.semantik.common; public class ReadZipException extends RuntimeException { diff --git a/common/src/main/java/de/ozgcloud/eingang/common/zip/ZipAttachmentReader.java b/semantik-adapter/src/main/java/de/ozgcloud/eingang/semantik/common/ZipAttachmentReader.java similarity index 99% rename from common/src/main/java/de/ozgcloud/eingang/common/zip/ZipAttachmentReader.java rename to semantik-adapter/src/main/java/de/ozgcloud/eingang/semantik/common/ZipAttachmentReader.java index 8f060cb8fa68e7045e788920dc9d5e6364fed2c1..02b4e95012b0343aa44ebd79f6f0305135ad9234 100644 --- a/common/src/main/java/de/ozgcloud/eingang/common/zip/ZipAttachmentReader.java +++ b/semantik-adapter/src/main/java/de/ozgcloud/eingang/semantik/common/ZipAttachmentReader.java @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -package de.ozgcloud.eingang.common.zip; +package de.ozgcloud.eingang.semantik.common; import java.io.File; import java.io.FileInputStream; diff --git a/semantik-adapter/src/main/java/de/ozgcloud/eingang/semantik/enginebased/formsolutions/FormSolutionsFilesMapper.java b/semantik-adapter/src/main/java/de/ozgcloud/eingang/semantik/enginebased/formsolutions/FormSolutionsFilesMapper.java index d63b1e4f5a502a5efce7f280b107422e0ee83a64..6de90deb9ebc890fba588b4f4ea60c983804c300 100644 --- a/semantik-adapter/src/main/java/de/ozgcloud/eingang/semantik/enginebased/formsolutions/FormSolutionsFilesMapper.java +++ b/semantik-adapter/src/main/java/de/ozgcloud/eingang/semantik/enginebased/formsolutions/FormSolutionsFilesMapper.java @@ -33,7 +33,7 @@ import org.springframework.stereotype.Component; import de.ozgcloud.eingang.common.formdata.FormData; import de.ozgcloud.eingang.common.formdata.IncomingFile; import de.ozgcloud.eingang.common.formdata.IncomingFileGroup; -import de.ozgcloud.eingang.common.zip.ZipAttachmentReader; +import de.ozgcloud.eingang.semantik.common.ZipAttachmentReader; import de.ozgcloud.eingang.semantik.enginebased.FilesMapperHelper; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -110,4 +110,4 @@ class FormSolutionsFilesMapper implements FormSolutionsEngineBasedMapper { return ZipAttachmentReader.from(zipFile.getFile(), zipFile.getName()); } } -} \ No newline at end of file +} diff --git a/common/src/test/java/de/ozgcloud/eingang/common/zip/ZipAttachmentReaderTest.java b/semantik-adapter/src/test/java/de/ozgcloud/eingang/semantik/common/ZipAttachmentReaderTest.java similarity index 99% rename from common/src/test/java/de/ozgcloud/eingang/common/zip/ZipAttachmentReaderTest.java rename to semantik-adapter/src/test/java/de/ozgcloud/eingang/semantik/common/ZipAttachmentReaderTest.java index 8cd2ad76c5b602b4b4e9096211afca5621c8569b..3f36987c5cfe0ff4b4bb6777c7d4a595c7c43dc4 100644 --- a/common/src/test/java/de/ozgcloud/eingang/common/zip/ZipAttachmentReaderTest.java +++ b/semantik-adapter/src/test/java/de/ozgcloud/eingang/semantik/common/ZipAttachmentReaderTest.java @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -package de.ozgcloud.eingang.common.zip; +package de.ozgcloud.eingang.semantik.common; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; @@ -317,4 +317,4 @@ class ZipAttachmentReaderTest { private static String getTmpDirectoryPath() { return TMP_DIRECTORY_PATH.endsWith("/") ? TMP_DIRECTORY_PATH.substring(0, TMP_DIRECTORY_PATH.length() - 1) : TMP_DIRECTORY_PATH; } -} \ No newline at end of file +} diff --git a/common/src/test/resources/zip-file-0.txt b/semantik-adapter/src/test/resources/zip-file-0.txt similarity index 100% rename from common/src/test/resources/zip-file-0.txt rename to semantik-adapter/src/test/resources/zip-file-0.txt diff --git a/common/src/test/resources/zip-file-1.txt b/semantik-adapter/src/test/resources/zip-file-1.txt similarity index 100% rename from common/src/test/resources/zip-file-1.txt rename to semantik-adapter/src/test/resources/zip-file-1.txt diff --git a/xta-adapter/src/main/java/de/ozgcloud/eingang/xta/XtaIncomingFilesMapper.java b/xta-adapter/src/main/java/de/ozgcloud/eingang/xta/XtaIncomingFilesMapper.java index e851176d883d602d13ddf2c04df730b2baa31d70..34a807f7001122a0561f319e767c06b08904f25b 100644 --- a/xta-adapter/src/main/java/de/ozgcloud/eingang/xta/XtaIncomingFilesMapper.java +++ b/xta-adapter/src/main/java/de/ozgcloud/eingang/xta/XtaIncomingFilesMapper.java @@ -23,20 +23,26 @@ package de.ozgcloud.eingang.xta; -import de.ozgcloud.eingang.common.formdata.IncomingFile; -import de.ozgcloud.eingang.common.zip.ZipAttachmentReader; -import lombok.extern.log4j.Log4j2; -import org.springframework.stereotype.Component; - import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Stream; +import org.springframework.stereotype.Component; + +import de.ozgcloud.eingang.common.formdata.IncomingFile; +import de.ozgcloud.eingang.xta.zip.ZipFileExtractor; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + @Log4j2 @Component +@RequiredArgsConstructor class XtaIncomingFilesMapper { + + private final ZipFileExtractor zipFileExtractor; + public static final String ZIP_CONTENT_TYPE = "application/zip"; static final Predicate<IncomingFile> IS_ZIP_FILE = contentType -> ZIP_CONTENT_TYPE.equals(contentType.getContentType()); @@ -65,14 +71,13 @@ class XtaIncomingFilesMapper { Stream<IncomingFile> extractZip(IncomingFile incomingFile) { if (IS_ZIP_FILE.test(incomingFile)) { try { - List<IncomingFile> extractedZips = ZipAttachmentReader.from(incomingFile.getContentStream(), incomingFile.getName()).readContent(); + List<IncomingFile> extractedZips = zipFileExtractor.extractIncomingFilesSafely(incomingFile); return extractedZips.stream(); } catch (RuntimeException e) { LOG.error("Cannot read source ZIP. Not extracting file", e); return Stream.of(incomingFile); } - } - else { + } else { return Stream.of(incomingFile); } } diff --git a/xta-adapter/src/main/java/de/ozgcloud/eingang/xta/zip/ReadableZipEntry.java b/xta-adapter/src/main/java/de/ozgcloud/eingang/xta/zip/ReadableZipEntry.java new file mode 100644 index 0000000000000000000000000000000000000000..f82f0090a248495ff4f46194b3e2344f807f5294 --- /dev/null +++ b/xta-adapter/src/main/java/de/ozgcloud/eingang/xta/zip/ReadableZipEntry.java @@ -0,0 +1,23 @@ +package de.ozgcloud.eingang.xta.zip; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import lombok.Builder; + +@Builder +record ReadableZipEntry(ZipEntry zipEntry, ZipFile parentZip) { + public InputStream getInputStream() throws IOException { + return parentZip.getInputStream(zipEntry); + } + + public Long getSize() { + return zipEntry.getSize(); + } + + public String getName() { + return zipEntry.getName(); + } +} diff --git a/xta-adapter/src/main/java/de/ozgcloud/eingang/xta/zip/ZipFileExtractor.java b/xta-adapter/src/main/java/de/ozgcloud/eingang/xta/zip/ZipFileExtractor.java new file mode 100644 index 0000000000000000000000000000000000000000..255f7eac2b3562f77724382c1216529b2c06ff0d --- /dev/null +++ b/xta-adapter/src/main/java/de/ozgcloud/eingang/xta/zip/ZipFileExtractor.java @@ -0,0 +1,100 @@ +package de.ozgcloud.eingang.xta.zip; + +import java.io.File; +import java.io.IOException; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.springframework.stereotype.Component; +import org.springframework.util.MimeTypeUtils; + +import de.ozgcloud.common.binaryfile.TempFileUtils; +import de.ozgcloud.eingang.common.errorhandling.TechnicalException; +import de.ozgcloud.eingang.common.formdata.IncomingFile; + +// TODO Resolve code duplication with ZipAttachmentReader in semantik-adapter common.zip +// Note: In contrast to the ZipAttachmentReader, here, the zip file is not included in list of extracted files +// Further, there is no ZIP_MAX_THRESHOLD detection. +@Component +public class ZipFileExtractor { + + static final int ZIP_MAX_TOTAL_SIZE = 500 * 1024 * 1024; + static final int ZIP_MAX_ENTRIES = 100; + + public List<IncomingFile> extractIncomingFilesSafely(IncomingFile zipIncomingFile) { + var zipFile = zipIncomingFile.getFile(); + verifySizeLimit(zipFile); + return extractIncomingFiles(zipFile); + } + + void verifySizeLimit(File zipFile) { + var totalSize = sumUncompressedEntrySizes(zipFile); + if (totalSize > ZIP_MAX_TOTAL_SIZE) { + throw new TechnicalException("Expect uncompressed size %s to be smaller than %d!".formatted(totalSize, ZIP_MAX_TOTAL_SIZE)); + } + } + + Long sumUncompressedEntrySizes(File zipFile) { + return mapZipEntries(zipFile, ReadableZipEntry::getSize) + .stream() + .mapToLong(Long::longValue) + .sum(); + } + + List<IncomingFile> extractIncomingFiles(File zipFile) { + return mapZipEntries(zipFile, this::mapReadableEntryToIncomingFile); + } + + private IncomingFile mapReadableEntryToIncomingFile(ReadableZipEntry entry) { + File file; + try (var inputStream = entry.getInputStream()) { + file = TempFileUtils.writeTmpFile(inputStream); + } catch (IOException e) { + throw new TechnicalException("Failed reading zip file element %s!".formatted(entry.getName()), e); + } + return createIncomingFile(file, entry.zipEntry()); + } + + <T> List<T> mapZipEntries(File zipFile, Function<ReadableZipEntry, T> mappingFunction) { + try (ZipFile zip = new ZipFile(zipFile)) { + var index = new AtomicInteger(); + var mappedElements = new ArrayList<T>(); + zip.stream().forEach(element -> { + if (index.getAndIncrement() >= ZIP_MAX_ENTRIES) { + throw new TechnicalException("Expect zip files to have at most %d entries!".formatted(ZIP_MAX_ENTRIES)); + } + mappedElements.add( + mappingFunction.apply( + ReadableZipEntry.builder() + .parentZip(zip) + .zipEntry(element) + .build() + ) + ); + }); + return mappedElements; + } catch (IOException e) { + throw new TechnicalException("Failed reading zip file!", e); + } + } + + IncomingFile createIncomingFile(File file, ZipEntry zipEntry) { + return IncomingFile.builder() + .name(zipEntry.getName()) + .size(zipEntry.getSize()) + .contentType(getContentType(zipEntry.getName())) + .file(file) + .build(); + } + + String getContentType(String name) { + Objects.requireNonNull(name); + return Objects.requireNonNullElse(URLConnection.guessContentTypeFromName(name), MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE); + } +} diff --git a/xta-adapter/src/test/java/de/ozgcloud/eingang/xta/XtaIncomingFilesMapperTest.java b/xta-adapter/src/test/java/de/ozgcloud/eingang/xta/XtaIncomingFilesMapperTest.java index da3cdcceeefb8b0f298fb7166bd200fc28b335bc..0dfa769d688d5bab27fce58f4673282a7e86caac 100644 --- a/xta-adapter/src/test/java/de/ozgcloud/eingang/xta/XtaIncomingFilesMapperTest.java +++ b/xta-adapter/src/test/java/de/ozgcloud/eingang/xta/XtaIncomingFilesMapperTest.java @@ -24,27 +24,28 @@ package de.ozgcloud.eingang.xta; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import de.ozgcloud.eingang.common.formdata.IncomingFile; -import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory; -import de.ozgcloud.eingang.common.zip.ZipAttachmentReader; -import lombok.SneakyThrows; +import java.util.List; +import java.util.stream.Stream; + import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; +import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.Spy; -import java.io.InputStream; -import java.util.List; -import java.util.stream.IntStream; -import java.util.stream.Stream; +import de.ozgcloud.eingang.common.formdata.IncomingFile; +import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory; +import de.ozgcloud.eingang.xta.zip.ZipFileExtractor; class XtaIncomingFilesMapperTest { @Spy - private XtaIncomingFilesMapper mapper = new XtaIncomingFilesMapper(); + @InjectMocks + private XtaIncomingFilesMapper mapper; + + @Mock + private ZipFileExtractor extractor; private static final String ZIP_CONTENT_TYPE = "application/zip"; @@ -106,23 +107,29 @@ class XtaIncomingFilesMapperTest { @Nested class TestExtractZip { + @Mock + IncomingFile outFile1; + + @Mock + IncomingFile outFile2; + + @Test void shouldExtractZipFiles() { - try (var zipAttachment = Mockito.mockStatic(ZipAttachmentReader.class)) { - var zipAttachmentReaderMock = initZipRepresentationMocks(zipAttachment); + var expectedExtractedFiles = List.of(outFile1, outFile2); + var zipFile = createTestIncomingFile(); + when(extractor.extractIncomingFilesSafely(zipFile)).thenReturn(expectedExtractedFiles); - var extractedFiles = mapper.extractZip(createTestIncomingFile()).toList(); + var extractedFiles = mapper.extractZip(zipFile).toList(); - verify(zipAttachmentReaderMock).readContent(); - assertThat(extractedFiles).hasSize(2); - } + assertThat(extractedFiles).isEqualTo(expectedExtractedFiles); } IncomingFile createTestIncomingFile() { return IncomingFileTestFactory.createBuilder() - .name("attachments.zip") - .contentType(ZIP_CONTENT_TYPE) - .build(); + .name("attachments.zip") + .contentType(ZIP_CONTENT_TYPE) + .build(); } @Test @@ -141,12 +148,4 @@ class XtaIncomingFilesMapperTest { assertThat(XtaIncomingFilesMapper.IS_ZIP_FILE.test(IncomingFileTestFactory.createBuilder().contentType(ZIP_CONTENT_TYPE).build())) .isTrue(); } - - @SneakyThrows - private static ZipAttachmentReader initZipRepresentationMocks(MockedStatic<ZipAttachmentReader> zipAttachmentMock) { - var contentEntries = IntStream.range(0, 2).boxed().map(i -> IncomingFileTestFactory.createBuilder().name(i.toString()).build()).toList(); - ZipAttachmentReader mock = when(mock(ZipAttachmentReader.class).readContent()).thenReturn(contentEntries).getMock(); - zipAttachmentMock.when(() -> ZipAttachmentReader.from(any(InputStream.class), any())).thenReturn(mock); - return mock; - } } diff --git a/xta-adapter/src/test/java/de/ozgcloud/eingang/xta/zip/ZipFileExtractorTest.java b/xta-adapter/src/test/java/de/ozgcloud/eingang/xta/zip/ZipFileExtractorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6a4e0c3d8521576f48e1bcc2b8d51645eeba68f2 --- /dev/null +++ b/xta-adapter/src/test/java/de/ozgcloud/eingang/xta/zip/ZipFileExtractorTest.java @@ -0,0 +1,329 @@ +package de.ozgcloud.eingang.xta.zip; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.springframework.util.MimeTypeUtils; + +import de.ozgcloud.common.binaryfile.TempFileUtils; +import de.ozgcloud.eingang.common.errorhandling.TechnicalException; +import de.ozgcloud.eingang.common.formdata.IncomingFile; +import lombok.Builder; +import lombok.Getter; + +class ZipFileExtractorTest { + + @Spy + @InjectMocks + private ZipFileExtractor extractor; + + @DisplayName("extract incoming files safely") + @Nested + class TestExtractIncomingFilesWithSizeLimit { + @Mock + IncomingFile incomingZipFile; + + @Mock + File zipFile; + + @Mock + IncomingFile outIncomingFile; + + List<IncomingFile> outIncomingFiles; + + @BeforeEach + void mock() { + outIncomingFiles = List.of(outIncomingFile); + + when(incomingZipFile.getFile()).thenReturn(zipFile); + doNothing().when(extractor).verifySizeLimit(zipFile); + doReturn(outIncomingFiles).when(extractor).extractIncomingFiles(zipFile); + } + + @DisplayName("should call verify size limit") + @Test + void shouldCallVerifySizeLimit() { + extractor.extractIncomingFilesSafely(incomingZipFile); + + verify(extractor).verifySizeLimit(zipFile); + } + + @DisplayName("should return") + @Test + void shouldReturn() { + var output = extractor.extractIncomingFilesSafely(incomingZipFile); + + assertThat(output).isEqualTo(outIncomingFiles); + } + } + + @DisplayName("verify size limit") + @Nested + class TestVerifySizeLimit { + @Mock + File zipFile; + + @DisplayName("should return") + @Test + void shouldReturn() { + doReturn((long) ZipFileExtractor.ZIP_MAX_TOTAL_SIZE).when(extractor).sumUncompressedEntrySizes(zipFile); + + extractor.verifySizeLimit(zipFile); + } + + @DisplayName("should throw if limit exceeded") + @Test + void shouldThrowIfLimitExceeded() { + doReturn((long) ZipFileExtractor.ZIP_MAX_TOTAL_SIZE + 1).when(extractor).sumUncompressedEntrySizes(zipFile); + + assertThatThrownBy(() -> extractor.verifySizeLimit(zipFile)) + .isInstanceOf(TechnicalException.class); + } + } + + @DisplayName("extract incoming files") + @Nested + class TestExtractIncomingFiles { + + private File zipFile; + + @BeforeEach + void mock() { + zipFile = createTempZipFile(fromMap(Map.of( + "file1.pdf", "file content1", + "file2.xml", "<root></root>", + "file3.png", "" + ))); + } + + @DisplayName("should contain content") + @Test + void shouldContainContent() { + var extractedFiles = extractor.extractIncomingFiles(zipFile); + + var contents = extractedFiles.stream().map(f -> { + try { + return Files.readString(f.getFile().toPath()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).toList(); + assertThat(contents).containsExactlyInAnyOrder("file content1", "<root></root>", ""); + } + + @DisplayName("should have names") + @Test + void shouldHaveNames() { + var extractedFiles = extractor.extractIncomingFiles(zipFile); + + var names = extractedFiles.stream().map(IncomingFile::getName).toList(); + assertThat(names).containsExactlyInAnyOrder("file1.pdf", "file2.xml", "file3.png"); + } + + @DisplayName("should have content types") + @Test + void shouldHaveContentTypes() { + var extractedFiles = extractor.extractIncomingFiles(zipFile); + + var names = extractedFiles.stream().map(IncomingFile::getContentType).toList(); + assertThat(names).containsExactlyInAnyOrder("application/pdf", "application/xml", "image/png"); + } + + private List<TestZipEntry> fromMap(Map<String, String> entries) { + return entries.entrySet().stream().map(kv -> TestZipEntry.builder() + .name(kv.getKey()) + .content(kv.getValue()) + .build()) + .toList(); + } + } + + @DisplayName("create incoming file") + @Nested + class TestCreateIncomingFile { + @Mock + File file; + + @Mock + ZipEntry zipEntry; + + private static final String NAME = "filename.name"; + private static final Long SIZE = 5L; + private static final String CONTENT_TYPE = "some/content"; + + @BeforeEach + void mock() { + when(zipEntry.getName()).thenReturn(NAME); + when(zipEntry.getSize()).thenReturn(SIZE); + doReturn(CONTENT_TYPE).when(extractor).getContentType(NAME); + } + + @DisplayName("should have name") + @Test + void shouldHaveName() { + var incomingFile = create(); + + assertThat(incomingFile.getName()).isEqualTo(NAME); + } + + @DisplayName("should have size") + @Test + void shouldHaveSize() { + var incomingFile = create(); + + assertThat(incomingFile.getSize()).isEqualTo(SIZE); + } + + @DisplayName("should have content type") + @Test + void shouldHaveContentType() { + var incomingFile = create(); + + assertThat(incomingFile.getContentType()).isEqualTo(CONTENT_TYPE); + } + + @DisplayName("should have file") + @Test + void shouldHaveFile() { + var incomingFile = create(); + + assertThat(incomingFile.getFile()).isEqualTo(file); + } + + private IncomingFile create() { + return extractor.createIncomingFile(file, zipEntry); + } + } + + + @DisplayName("sum uncompressed entry size") + @Nested + class TestSumUncompressedEntrySize { + @DisplayName("should return size") + @Test + void shouldReturnSize() { + var sizes = IntStream.range(100, 110).boxed().toList(); + var expectedSum = sizes.stream().mapToLong(Integer::longValue).sum(); + var zipFile = createTempZipFile(sizes.stream() + .map(size -> TestZipEntry.builder() + .name("somefilewithsize%d".formatted(size)) + .content("A".repeat(size)) + .build() + ).toList()); + + var sum = extractor.sumUncompressedEntrySizes(zipFile); + + assertThat(sum).isEqualTo(expectedSum); + } + } + + + @Nested + class TestContentType { + + @Test + void shouldReturnDefaultWhenNullString() { + assertThrows(NullPointerException.class, () -> extractor.getContentType(null)); + } + + @Test + void shouldReturnDefaultWhenEmptyString() { + var contentType = extractor.getContentType(StringUtils.EMPTY); + + assertThat(contentType).isEqualTo(MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE); + } + + @Test + void shouldReturnDefaultWhenSpaceString() { + var contentType = extractor.getContentType(StringUtils.SPACE); + + assertThat(contentType).isEqualTo(MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE); + } + + @Test + void shouldGetContentType() { + var fileNames = List.of("1.xml", "2.txt"); + + var contentTypes = fileNames.stream().map(extractor::getContentType).toList(); + + assertThat(contentTypes).containsExactlyInAnyOrder(MimeTypeUtils.APPLICATION_XML_VALUE, MimeTypeUtils.TEXT_PLAIN_VALUE); + } + } + + + @DisplayName("map zip entries") + @Nested + class TestMapZipEntries { + + @DisplayName("should throw if max entries exceeded") + @Test + void shouldThrowIfMaxEntriesExceeded() { + var zipWithTooManyEntries = createTempZipFile(IntStream.range(0, ZipFileExtractor.ZIP_MAX_ENTRIES + 1) + .mapToObj(i -> TestZipEntry.builder() + .name("test%d.txt".formatted(i)) + .content("test file %d".formatted(i)) + .build() + ).toList()); + + assertThatThrownBy(() -> extractor.mapZipEntries(zipWithTooManyEntries, entry -> null)) + .isInstanceOf(TechnicalException.class); + } + + @DisplayName("should map with mapping function") + @Test + void shouldMapWithMappingFunction() { + var expectedNumberList = IntStream.range(0, ZipFileExtractor.ZIP_MAX_ENTRIES).boxed().toList(); + var zipFile = createTempZipFile(expectedNumberList.stream() + .map(i -> TestZipEntry.builder() + .name("%d".formatted(i)) + .content("some content") + .build() + ).toList()); + + var numberList = extractor.mapZipEntries(zipFile, entry -> Integer.parseInt(entry.getName())); + + assertThat(numberList).isEqualTo(expectedNumberList); + } + } + + @Builder + @Getter + static class TestZipEntry { + private String name; + private String content; + } + + private File createTempZipFile(List<TestZipEntry> testZipEntries) { + var file = TempFileUtils.createTmpFile().toFile(); + try (var zipOutputStream = new ZipOutputStream(new FileOutputStream(file))) { + for (TestZipEntry entry : testZipEntries) { + zipOutputStream.putNextEntry(new ZipEntry(entry.getName())); + zipOutputStream.write(entry.getContent().getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + } + return file; + } catch (IOException e) { + throw new RuntimeException("Failed to create temporary zip file", e); + } + } +}