diff --git a/src/main/java/com/condation/cms/modules/backup/TarGzPacker.java b/src/main/java/com/condation/cms/modules/backup/TarGzPacker.java index 7a89495..26d03a6 100644 --- a/src/main/java/com/condation/cms/modules/backup/TarGzPacker.java +++ b/src/main/java/com/condation/cms/modules/backup/TarGzPacker.java @@ -43,7 +43,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; -import org.apache.commons.io.IOUtils; +import org.apache.commons.compress.utils.IOUtils; /** * diff --git a/src/test/java/com/condation/cms/modules/backup/BackupLifecycleExtensionTest.java b/src/test/java/com/condation/cms/modules/backup/BackupLifecycleExtensionTest.java new file mode 100644 index 0000000..7758ecd --- /dev/null +++ b/src/test/java/com/condation/cms/modules/backup/BackupLifecycleExtensionTest.java @@ -0,0 +1,169 @@ +package com.condation.cms.modules.backup; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.condation.cms.api.extensions.server.ServerLifecycleExtensionPoint; +import com.condation.cms.api.feature.features.InjectorFeature; +import com.condation.cms.api.hooks.HookSystem; +import com.condation.cms.api.scheduler.CronJobScheduler; +import com.condation.cms.api.scheduler.ScheduledTask; +import com.condation.cms.api.utils.PathUtil; +import com.condation.cms.api.utils.ServerUtil; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.name.Names; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; + +public class BackupLifecycleExtensionTest { + + private BackupLifecycleExtension extension; + private CronJobScheduler scheduler; + private HookSystem hookSystem; + private Path tempDir; + private Context context; + private InjectorFeature injectorFeature; + private Injector injector; + + @BeforeEach + public void setUp(@TempDir Path tempDir) throws Exception { + this.tempDir = tempDir; + + // Mocks + scheduler = mock(CronJobScheduler.class); + hookSystem = mock(HookSystem.class); + context = mock(ServerLifecycleExtensionPoint.Context.class); + injectorFeature = mock(InjectorFeature.class); + injector = mock(Injector.class); + + // Stubbing + when(context.get(InjectorFeature.class)).thenReturn(injectorFeature); + when(injectorFeature.injector()).thenReturn(injector); + when(injector.getInstance(Key.get(CronJobScheduler.class, Names.named("server")))).thenReturn(scheduler); + when(injector.getInstance(HookSystem.class)).thenReturn(hookSystem); + + // Class under test + extension = new BackupLifecycleExtension(); + + // Inject mock context using reflection + Field contextField = ServerLifecycleExtensionPoint.class.getDeclaredField("context"); + contextField.setAccessible(true); + contextField.set(extension, context); + } + + private void runBackup(Configuration config) { + try (MockedStatic mockedConfigLoader = mockStatic(ConfigLoader.class); + MockedStatic mockedServerUtil = mockStatic(ServerUtil.class); + MockedStatic mockedPathUtil = mockStatic(PathUtil.class)) { + + mockedConfigLoader.when(ConfigLoader::load).thenReturn(Optional.of(config)); + mockedServerUtil.when(ServerUtil::getHome).thenReturn(tempDir); + mockedPathUtil.when(() -> PathUtil.isChild(any(), any())).thenReturn(true); + + ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(ScheduledTask.class); + extension.started(); + verify(scheduler).schedule(anyString(), anyString(), taskCaptor.capture()); + taskCaptor.getValue().execute(null); + } + } + + @Test + public void testBackupSkipsPostProcessingWhenNoChanges() throws IOException, NoSuchAlgorithmException { + // Arrange + Path sourceFile = Files.createFile(tempDir.resolve("source.txt")); + Files.writeString(sourceFile, "This is a test file."); + + Path tempBackupFile = tempDir.resolve("temp.tar.gz"); + TarGzPacker.createTarGz(tempDir, tempBackupFile.toFile(), Collections.singletonList(sourceFile)); + String checksum = BackupUtil.calculateSHA256(tempBackupFile); + Files.delete(tempBackupFile); + + Path checksumFile = tempDir.resolve("testBackup.sha256"); + Files.writeString(checksumFile, checksum); + + Configuration.Backup backupConfig = new Configuration.Backup(); + backupConfig.setName("testBackup"); + backupConfig.setEnabled(true); + backupConfig.setProcessOnlyOnChange(true); + backupConfig.setTarget(tempDir.toString()); + backupConfig.setCron("0 0 * * *"); + backupConfig.setInclude_files(Collections.singletonList(sourceFile.toString())); + + Configuration config = new Configuration(); + config.setBackups(Collections.singletonList(backupConfig)); + + // Act + runBackup(config); + + // Assert + verify(hookSystem, never()).execute(anyString(), any(Map.class)); + try (Stream files = Files.list(tempDir)) { + long backupFileCount = files.filter(p -> p.toString().endsWith(".tar.gz")).count(); + assertEquals(0, backupFileCount, "No backup file should exist as it should be deleted."); + } + } + + @Test + public void testBackupRunsPostProcessingWhenChanges() throws IOException, NoSuchAlgorithmException { + // Arrange + Path sourceFile = Files.createFile(tempDir.resolve("source.txt")); + Files.writeString(sourceFile, "This is a test file."); + + Path tempBackupFile = tempDir.resolve("temp.tar.gz"); + TarGzPacker.createTarGz(tempDir, tempBackupFile.toFile(), Collections.singletonList(sourceFile)); + String checksum = BackupUtil.calculateSHA256(tempBackupFile); + Files.delete(tempBackupFile); + + Path checksumFile = tempDir.resolve("testBackup.sha256"); + Files.writeString(checksumFile, checksum); + + // Change the source file so the new backup has a different checksum + Files.writeString(sourceFile, "This is a modified test file."); + + Configuration.Backup backupConfig = new Configuration.Backup(); + backupConfig.setName("testBackup"); + backupConfig.setEnabled(true); + backupConfig.setProcessOnlyOnChange(true); + backupConfig.setTarget(tempDir.toString()); + backupConfig.setCron("0 0 * * *"); + backupConfig.setInclude_files(Collections.singletonList(sourceFile.toString())); + + Configuration config = new Configuration(); + config.setBackups(Collections.singletonList(backupConfig)); + + // Act + runBackup(config); + + // Assert + verify(hookSystem, times(1)).execute(eq("module/backup/postprocess"), any(Map.class)); + try (Stream files = Files.list(tempDir)) { + long backupFileCount = files.filter(p -> p.toString().endsWith(".tar.gz")).count(); + assertEquals(1, backupFileCount, "A new backup file should exist."); + } + assertTrue(Files.exists(checksumFile)); + } +}