diff --git a/src/main/java/org/frankframework/insights/release/ReleaseArtifactService.java b/src/main/java/org/frankframework/insights/release/ReleaseArtifactService.java index 04619088..6ff9a18f 100644 --- a/src/main/java/org/frankframework/insights/release/ReleaseArtifactService.java +++ b/src/main/java/org/frankframework/insights/release/ReleaseArtifactService.java @@ -4,6 +4,7 @@ import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -12,6 +13,7 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.zip.ZipFile; import lombok.extern.slf4j.Slf4j; import org.frankframework.insights.common.util.TagNameSanitizer; import org.frankframework.insights.release.releasecleanup.FileTreeDeleter; @@ -44,17 +46,29 @@ public Path downloadReleaseZipToPvc(String tagName) throws IOException { Path zipPath = archiveDir.resolve(safeFileName); if (Files.exists(zipPath)) { - return zipPath; + if (isValidZip(zipPath)) { + return zipPath; + } + log.warn( + "Cached ZIP for {} is corrupt (likely a truncated download), re-downloading: {}", tagName, zipPath); + Files.deleteIfExists(zipPath); } log.info("ZIP not found op storage, downloading: {}", tagName); String url = String.format(GITHUB_ZIP_URL_FORMAT, tagName); - try (InputStream in = new URI(url).toURL().openStream()) { - Files.copy(in, zipPath, StandardCopyOption.REPLACE_EXISTING); + Path tempZipPath = archiveDir.resolve(safeFileName + ".tmp"); + try (InputStream in = openDownloadStream(url)) { + Files.copy(in, tempZipPath, StandardCopyOption.REPLACE_EXISTING); + + if (!isValidZip(tempZipPath)) { + throw new IOException("Downloaded ZIP for " + tagName + " is not a valid archive"); + } + + moveIntoPlace(tempZipPath, zipPath); log.info("ZIP succesfully downloading to storage: {}", zipPath); } catch (IOException | URISyntaxException e) { try { - Files.deleteIfExists(zipPath); + Files.deleteIfExists(tempZipPath); } catch (IOException ignored) { } throw new IOException("Could not download release ZIP for " + tagName, e); @@ -62,6 +76,26 @@ public Path downloadReleaseZipToPvc(String tagName) throws IOException { return zipPath; } + private void moveIntoPlace(Path tempZipPath, Path zipPath) throws IOException { + try { + Files.move(tempZipPath, zipPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException _) { + Files.move(tempZipPath, zipPath, StandardCopyOption.REPLACE_EXISTING); + } + } + + protected InputStream openDownloadStream(String url) throws IOException, URISyntaxException { + return new URI(url).toURL().openStream(); + } + + protected boolean isValidZip(Path path) { + try (ZipFile _ = new ZipFile(path.toFile())) { + return true; + } catch (IOException _) { + return false; + } + } + public void deleteObsoleteReleaseArtifacts() { List allReleases = releaseRepository.findAll(); Set activeReleaseTags = @@ -73,18 +107,19 @@ public void deleteObsoleteReleaseArtifacts() { try (Stream files = Files.list(dir)) { files.forEach(path -> { String fileName = path.getFileName().toString(); - if (fileName.endsWith(".zip")) { - String tagName = fileName.replace(".zip", ""); - if (!activeReleaseTags.contains(tagName)) { - try { - log.info("Deletion of obsolete release artifact: {}", fileName); - fileTreeDeleter.deleteTreeRecursively(path); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("Interrupted while deleting file: {}", fileName, e); - } catch (Exception e) { - log.error("Could not delete file: {}", fileName, e); - } + boolean isOrphanedTempFile = fileName.endsWith(".tmp"); + boolean isObsoleteZip = + fileName.endsWith(".zip") && !activeReleaseTags.contains(fileName.replace(".zip", "")); + + if (isOrphanedTempFile || isObsoleteZip) { + try { + log.info("Deletion of obsolete release artifact: {}", fileName); + fileTreeDeleter.deleteTreeRecursively(path); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Interrupted while deleting file: {}", fileName, e); + } catch (Exception e) { + log.error("Could not delete file: {}", fileName, e); } } }); diff --git a/src/test/java/org/frankframework/insights/release/ReleaseArtifactServiceIntegrationTest.java b/src/test/java/org/frankframework/insights/release/ReleaseArtifactServiceIntegrationTest.java index 17cab644..91d3a3b0 100644 --- a/src/test/java/org/frankframework/insights/release/ReleaseArtifactServiceIntegrationTest.java +++ b/src/test/java/org/frankframework/insights/release/ReleaseArtifactServiceIntegrationTest.java @@ -2,18 +2,28 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; /** @@ -32,7 +42,7 @@ public class ReleaseArtifactServiceIntegrationTest { @BeforeEach public void setUp() { - releaseArtifactService = new ReleaseArtifactService(tempDir.toString(), releaseRepository); + releaseArtifactService = Mockito.spy(new ReleaseArtifactService(tempDir.toString(), releaseRepository)); } private Release createRelease(String name, String tagName) { @@ -42,6 +52,78 @@ private Release createRelease(String name, String tagName) { return release; } + private byte[] createValidZipBytes() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos)) { + zos.putNextEntry(new ZipEntry("test.txt")); + zos.write("hello world".getBytes()); + zos.closeEntry(); + } + return baos.toByteArray(); + } + + @Test + public void downloadReleaseZipToPvc_whenCorruptFileInCache_shouldRedownloadAndReplace() throws Exception { + String tagName = "v8.0.5"; + Path corruptZip = tempDir.resolve("v8.0.5.zip"); + Files.writeString(corruptZip, "this is not a zip file"); + + InputStream mockStream = new ByteArrayInputStream(createValidZipBytes()); + doReturn(mockStream).when(releaseArtifactService).openDownloadStream(any()); + + Path downloaded = releaseArtifactService.downloadReleaseZipToPvc(tagName); + + assertTrue(Files.exists(downloaded), "Downloaded zip should exist"); + assertTrue(releaseArtifactService.isValidZip(downloaded), "Re-downloaded zip should be valid"); + assertFalse( + Files.exists(tempDir.resolve("v8.0.5.zip.tmp")), "Temp file should not remain after a successful move"); + } + + @Test + public void downloadReleaseZipToPvc_whenDownloadIsCorrupt_shouldThrowAndCleanUpTempFile() throws Exception { + String tagName = "v8.0.6"; + Path zip = tempDir.resolve("v8.0.6.zip"); + Path tempZip = tempDir.resolve("v8.0.6.zip.tmp"); + + InputStream corruptStream = new ByteArrayInputStream("bad download".getBytes()); + doReturn(corruptStream).when(releaseArtifactService).openDownloadStream(any()); + + assertThrows(IOException.class, () -> releaseArtifactService.downloadReleaseZipToPvc(tagName)); + + assertFalse(Files.exists(tempZip), "Temporary download file should be cleaned up on failure"); + assertFalse(Files.exists(zip), "Invalid download should never be cached under its final name"); + } + + @Test + public void deleteObsoleteReleaseArtifacts_whenOrphanedTempFilesExist_shouldDeleteThem() throws IOException { + Path orphanedTemp = tempDir.resolve("v8.0.5.zip.tmp"); + Path validZip = tempDir.resolve("v8.0.0.zip"); + + Files.writeString(orphanedTemp, "leftover temp"); + Files.writeString(validZip, "valid zip content"); + + Release validRelease = createRelease("8.0.0", "v8.0.0"); + when(releaseRepository.findAll()).thenReturn(List.of(validRelease)); + + releaseArtifactService.deleteObsoleteReleaseArtifacts(); + + assertFalse(Files.exists(orphanedTemp), "Orphaned temp file should be deleted"); + assertTrue(Files.exists(validZip), "Valid active zip should still exist"); + } + + @Test + public void downloadReleaseZipToPvc_whenValidZipAlreadyCached_shouldReturnItWithoutDownloading() + throws IOException, URISyntaxException { + String tagName = "v8.1.0"; + Path zipPath = tempDir.resolve("v8.1.0.zip"); + Files.write(zipPath, createValidZipBytes()); + + Path result = releaseArtifactService.downloadReleaseZipToPvc(tagName); + + assertEquals(zipPath, result); + Mockito.verify(releaseArtifactService, Mockito.never()).openDownloadStream(any()); + } + @Test public void deleteObsoleteReleaseArtifacts_whenObsoleteZipsExist_shouldDeleteThem() throws IOException { Path validZip = tempDir.resolve("v8.0.0.zip"); diff --git a/src/test/java/org/frankframework/insights/release/ReleaseArtifactServiceTest.java b/src/test/java/org/frankframework/insights/release/ReleaseArtifactServiceTest.java index 051bc2be..961eab94 100644 --- a/src/test/java/org/frankframework/insights/release/ReleaseArtifactServiceTest.java +++ b/src/test/java/org/frankframework/insights/release/ReleaseArtifactServiceTest.java @@ -1,14 +1,21 @@ package org.frankframework.insights.release; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; @@ -35,8 +42,9 @@ public class ReleaseArtifactServiceTest { @BeforeEach public void setUp() { - releaseArtifactService = new ReleaseArtifactService("/release-archive", releaseRepository); + releaseArtifactService = Mockito.spy(new ReleaseArtifactService("/release-archive", releaseRepository)); mockedFiles = Mockito.mockStatic(Files.class); + Mockito.lenient().doReturn(true).when(releaseArtifactService).isValidZip(any()); } @AfterEach @@ -103,7 +111,7 @@ public void deleteObsoleteReleaseArtifacts_whenArchiveDirectoryDoesNotExist_shou } @Test - public void deleteObsoleteReleaseArtifacts_whenNoObsoleteZips_shouldNotDeleteAnything() throws IOException { + public void deleteObsoleteReleaseArtifacts_whenNoObsoleteZips_shouldNotDeleteAnything() { Release release1 = createRelease("7.8.0", "v7.8.0"); Release release2 = createRelease("8.0.0", "v8.0.0"); List releases = List.of(release1, release2); @@ -122,7 +130,7 @@ public void deleteObsoleteReleaseArtifacts_whenNoObsoleteZips_shouldNotDeleteAny } @Test - public void deleteObsoleteReleaseArtifacts_whenListDirectoriesFails_shouldHandleGracefully() throws IOException { + public void deleteObsoleteReleaseArtifacts_whenListDirectoriesFails_shouldHandleGracefully() { mockedFiles.when(() -> Files.exists(ARCHIVE_DIR)).thenReturn(true); when(releaseRepository.findAll()).thenReturn(new ArrayList<>()); mockedFiles.when(() -> Files.list(ARCHIVE_DIR)).thenThrow(new IOException("Permission denied")); @@ -133,7 +141,7 @@ public void deleteObsoleteReleaseArtifacts_whenListDirectoriesFails_shouldHandle } @Test - public void deleteObsoleteReleaseArtifacts_shouldIgnoreNonZipFiles() throws IOException { + public void deleteObsoleteReleaseArtifacts_shouldIgnoreNonZipFiles() { Release release1 = createRelease("8.0.0", "v8.0.0"); List releases = List.of(release1); @@ -166,7 +174,7 @@ public void downloadReleaseZipToPvc_whenArchiveDirectoryDoesNotExist_shouldCreat } @Test - public void deleteObsoleteReleaseArtifacts_whenObsoleteZipExists_shouldDeleteIt() throws IOException { + public void deleteObsoleteReleaseArtifacts_whenObsoleteZipExists_shouldDeleteIt() { Release release1 = createRelease("8.0.0", "v8.0.0"); List releases = List.of(release1); @@ -183,7 +191,7 @@ public void deleteObsoleteReleaseArtifacts_whenObsoleteZipExists_shouldDeleteIt( } @Test - public void deleteObsoleteReleaseArtifacts_whenAllZipsAreObsolete_shouldDeleteAll() throws IOException { + public void deleteObsoleteReleaseArtifacts_whenAllZipsAreObsolete_shouldDeleteAll() { List releases = new ArrayList<>(); Path obsoleteZip1 = ARCHIVE_DIR.resolve("v6.0.0.zip"); @@ -199,7 +207,7 @@ public void deleteObsoleteReleaseArtifacts_whenAllZipsAreObsolete_shouldDeleteAl } @Test - public void deleteObsoleteReleaseArtifacts_withMixedContent_shouldOnlyProcessZipFiles() throws IOException { + public void deleteObsoleteReleaseArtifacts_withMixedContent_shouldOnlyProcessZipFiles() { Release release1 = createRelease("8.0.0", "v8.0.0"); List releases = List.of(release1); @@ -219,7 +227,7 @@ public void deleteObsoleteReleaseArtifacts_withMixedContent_shouldOnlyProcessZip } @Test - public void deleteObsoleteReleaseArtifacts_withMultipleActiveReleases_shouldKeepAllActive() throws IOException { + public void deleteObsoleteReleaseArtifacts_withMultipleActiveReleases_shouldKeepAllActive() { Release release1 = createRelease("7.8.0", "v7.8.0"); Release release2 = createRelease("8.0.0", "v8.0.0"); Release release3 = createRelease("8.1.0", "v8.1.0"); @@ -240,7 +248,7 @@ public void deleteObsoleteReleaseArtifacts_withMultipleActiveReleases_shouldKeep } @Test - public void deleteObsoleteReleaseArtifacts_withEmptyDirectory_shouldHandleGracefully() throws IOException { + public void deleteObsoleteReleaseArtifacts_withEmptyDirectory_shouldHandleGracefully() { Release release1 = createRelease("8.0.0", "v8.0.0"); List releases = List.of(release1); @@ -267,24 +275,7 @@ public void downloadReleaseZipToPvc_withSpecialCharactersInTagName_shouldHandleC } @Test - public void deleteObsoleteReleaseArtifacts_withEmptyReleaseList_shouldDeleteAllZips() throws IOException { - List releases = new ArrayList<>(); - - Path zip1 = ARCHIVE_DIR.resolve("v1.0.0.zip"); - Path zip2 = ARCHIVE_DIR.resolve("v2.0.0.zip"); - Path zip3 = ARCHIVE_DIR.resolve("v3.0.0.zip"); - - mockedFiles.when(() -> Files.exists(ARCHIVE_DIR)).thenReturn(true); - when(releaseRepository.findAll()).thenReturn(releases); - mockedFiles.when(() -> Files.list(ARCHIVE_DIR)).thenReturn(Stream.of(zip1, zip2, zip3)); - - releaseArtifactService.deleteObsoleteReleaseArtifacts(); - - mockedFiles.verify(() -> Files.list(ARCHIVE_DIR)); - } - - @Test - public void deleteObsoleteReleaseArtifacts_withSingleActiveRelease_shouldKeepOnlyActive() throws IOException { + public void deleteObsoleteReleaseArtifacts_withEmptyReleaseList_shouldDeleteAllZips() { Release release = createRelease("9.0.0", "v9.0.0"); List releases = List.of(release); @@ -329,7 +320,7 @@ public void downloadReleaseZipToPvc_withSnapshotTag_shouldReturnCorrectPath() th } @Test - public void deleteObsoleteReleaseArtifacts_withOnlyActiveZips_shouldNotDeleteAny() throws IOException { + public void deleteObsoleteReleaseArtifacts_withOnlyActiveZips_shouldNotDeleteAny() { Release release1 = createRelease("7.0.0", "v7.0.0"); Release release2 = createRelease("7.5.0", "v7.5.0"); Release release3 = createRelease("8.0.0", "v8.0.0"); @@ -352,7 +343,7 @@ public void deleteObsoleteReleaseArtifacts_withOnlyActiveZips_shouldNotDeleteAny } @Test - public void deleteObsoleteReleaseArtifacts_withHiddenFiles_shouldIgnoreThem() throws IOException { + public void deleteObsoleteReleaseArtifacts_withHiddenFiles_shouldIgnoreThem() { Release release = createRelease("8.0.0", "v8.0.0"); List releases = List.of(release); @@ -370,7 +361,7 @@ public void deleteObsoleteReleaseArtifacts_withHiddenFiles_shouldIgnoreThem() th } @Test - public void deleteObsoleteReleaseArtifacts_withSubdirectories_shouldIgnoreThem() throws IOException { + public void deleteObsoleteReleaseArtifacts_withSubdirectories_shouldIgnoreThem() { Release release = createRelease("8.0.0", "v8.0.0"); List releases = List.of(release); @@ -414,7 +405,7 @@ public void downloadReleaseZipToPvc_withMilestoneTag_shouldReturnCorrectPath() t } @Test - public void deleteObsoleteReleaseArtifacts_withZipAndTarGz_shouldOnlyProcessZipFiles() throws IOException { + public void deleteObsoleteReleaseArtifacts_withZipAndTarGz_shouldOnlyProcessZipFiles() { Release release = createRelease("8.0.0", "v8.0.0"); List releases = List.of(release); @@ -432,7 +423,7 @@ public void deleteObsoleteReleaseArtifacts_withZipAndTarGz_shouldOnlyProcessZipF } @Test - public void deleteObsoleteReleaseArtifacts_withCaseVariations_shouldMatchCorrectly() throws IOException { + public void deleteObsoleteReleaseArtifacts_withCaseVariations_shouldMatchCorrectly() { Release release = createRelease("8.0.0", "v8.0.0"); List releases = List.of(release); @@ -449,7 +440,7 @@ public void deleteObsoleteReleaseArtifacts_withCaseVariations_shouldMatchCorrect } @Test - public void deleteObsoleteReleaseArtifacts_withManyObsoleteZips_shouldAttemptToDeleteAll() throws IOException { + public void deleteObsoleteReleaseArtifacts_withManyObsoleteZips_shouldAttemptToDeleteAll() { Release release = createRelease("10.0.0", "v10.0.0"); List releases = List.of(release); @@ -472,7 +463,7 @@ public void deleteObsoleteReleaseArtifacts_withManyObsoleteZips_shouldAttemptToD } @Test - public void deleteObsoleteReleaseArtifacts_withNullTagName_shouldHandleGracefully() throws IOException { + public void deleteObsoleteReleaseArtifacts_withNullTagName_shouldHandleGracefully() { Release releaseWithNullTag = new Release(); releaseWithNullTag.setName("Test Release"); releaseWithNullTag.setTagName(null); @@ -505,7 +496,7 @@ public void downloadReleaseZipToPvc_withNumericTag_shouldReturnCorrectPath() thr } @Test - public void deleteObsoleteReleaseArtifacts_withLogFiles_shouldIgnoreThem() throws IOException { + public void deleteObsoleteReleaseArtifacts_withLogFiles_shouldIgnoreThem() { Release release = createRelease("8.0.0", "v8.0.0"); List releases = List.of(release); @@ -523,7 +514,7 @@ public void deleteObsoleteReleaseArtifacts_withLogFiles_shouldIgnoreThem() throw } @Test - public void deleteObsoleteReleaseArtifacts_withPartialZipNames_shouldNotMatchWrongly() throws IOException { + public void deleteObsoleteReleaseArtifacts_withPartialZipNames_shouldNotMatchWrongly() { Release release = createRelease("8.0.0", "v8.0.0"); List releases = List.of(release); @@ -553,4 +544,101 @@ public void downloadReleaseZipToPvc_returnsExpectedZipPathFormat() throws IOExce Assertions.assertTrue(result.toString().endsWith(".zip")); Assertions.assertTrue(result.toString().contains(tagName)); } + + @Test + public void downloadReleaseZipToPvc_whenValidZipAlreadyExists_shouldReturnExistingPath() throws IOException { + String tagName = "v7.8.0"; + Path zipPath = ARCHIVE_DIR.resolve(tagName + ".zip"); + + mockedFiles.when(() -> Files.exists(ARCHIVE_DIR)).thenReturn(true); + mockedFiles.when(() -> Files.exists(zipPath)).thenReturn(true); + doReturn(true).when(releaseArtifactService).isValidZip(zipPath); + + Path result = releaseArtifactService.downloadReleaseZipToPvc(tagName); + + assertEquals(zipPath, result); + } + + @Test + public void downloadReleaseZipToPvc_whenCachedZipIsCorrupt_shouldDeleteAndRedownload() throws Exception { + String tagName = "v8.0.5"; + Path zipPath = ARCHIVE_DIR.resolve(tagName + ".zip"); + Path tempZipPath = ARCHIVE_DIR.resolve(tagName + ".zip.tmp"); + + mockedFiles.when(() -> Files.exists(ARCHIVE_DIR)).thenReturn(true); + mockedFiles.when(() -> Files.exists(zipPath)).thenReturn(true); + + doReturn(false).when(releaseArtifactService).isValidZip(zipPath); + doReturn(true).when(releaseArtifactService).isValidZip(tempZipPath); + + InputStream dummyStream = new ByteArrayInputStream(new byte[] {1, 2, 3}); + doReturn(dummyStream).when(releaseArtifactService).openDownloadStream(any()); + + Path result = releaseArtifactService.downloadReleaseZipToPvc(tagName); + + assertEquals(zipPath, result); + mockedFiles.verify(() -> Files.deleteIfExists(zipPath)); + } + + @Test + public void downloadReleaseZipToPvc_whenDownloadedZipIsInvalid_shouldThrowAndCleanUpTempFile() throws Exception { + String tagName = "v8.0.6"; + Path tempZipPath = ARCHIVE_DIR.resolve(tagName + ".zip.tmp"); + + mockedFiles.when(() -> Files.exists(ARCHIVE_DIR)).thenReturn(true); + + InputStream dummyStream = new ByteArrayInputStream(new byte[] {1, 2, 3}); + doReturn(dummyStream).when(releaseArtifactService).openDownloadStream(any()); + doReturn(false).when(releaseArtifactService).isValidZip(tempZipPath); + + assertThrows(IOException.class, () -> releaseArtifactService.downloadReleaseZipToPvc(tagName)); + mockedFiles.verify(() -> Files.deleteIfExists(tempZipPath)); + } + + @Test + public void downloadReleaseZipToPvc_whenAtomicMoveUnsupported_shouldFallBackToPlainMove() throws Exception { + String tagName = "v8.0.7"; + Path zipPath = ARCHIVE_DIR.resolve(tagName + ".zip"); + Path tempZipPath = ARCHIVE_DIR.resolve(tagName + ".zip.tmp"); + + mockedFiles.when(() -> Files.exists(ARCHIVE_DIR)).thenReturn(true); + + InputStream dummyStream = new ByteArrayInputStream(new byte[] {1, 2, 3}); + doReturn(dummyStream).when(releaseArtifactService).openDownloadStream(any()); + doReturn(true).when(releaseArtifactService).isValidZip(tempZipPath); + + mockedFiles + .when(() -> Files.move( + eq(tempZipPath), + eq(zipPath), + eq(StandardCopyOption.REPLACE_EXISTING), + eq(StandardCopyOption.ATOMIC_MOVE))) + .thenThrow(new AtomicMoveNotSupportedException( + tempZipPath.toString(), zipPath.toString(), "not supported on this filesystem")); + mockedFiles + .when(() -> Files.move(eq(tempZipPath), eq(zipPath), eq(StandardCopyOption.REPLACE_EXISTING))) + .thenReturn(zipPath); + + Path result = releaseArtifactService.downloadReleaseZipToPvc(tagName); + + assertEquals(zipPath, result); + mockedFiles.verify(() -> Files.move(eq(tempZipPath), eq(zipPath), eq(StandardCopyOption.REPLACE_EXISTING))); + } + + @Test + public void deleteObsoleteReleaseArtifacts_whenOrphanedTempFileExists_shouldAttemptToDeleteIt() { + Release release = createRelease("8.0.0", "v8.0.0"); + List releases = List.of(release); + + Path activeZip = ARCHIVE_DIR.resolve("v8.0.0.zip"); + Path orphanedTempFile = ARCHIVE_DIR.resolve("v7.0.0.zip.tmp"); + + mockedFiles.when(() -> Files.exists(ARCHIVE_DIR)).thenReturn(true); + when(releaseRepository.findAll()).thenReturn(releases); + mockedFiles.when(() -> Files.list(ARCHIVE_DIR)).thenReturn(Stream.of(activeZip, orphanedTempFile)); + + releaseArtifactService.deleteObsoleteReleaseArtifacts(); + + mockedFiles.verify(() -> Files.list(ARCHIVE_DIR)); + } }