From a0e8585c811ae2f22e8057917e8ab895bfac444b Mon Sep 17 00:00:00 2001 From: Robert Goldmann <deadlocker@gmx.de> Date: Sun, 8 Mar 2020 15:04:52 +0100 Subject: [PATCH] #455 - added tests for determination of files to delete --- pom.xml | 8 ++ .../database/DatabaseService.java | 57 +++++--- .../settings/SettingsController.java | 10 +- .../unit/database/DatabaseServiceTest.java | 133 ++++++++++++++++++ .../unit/helpers/LoggerTestUtil.java | 21 +++ src/test/resources/backups/empty/.gitkeep | 0 ...getMasterDatabase_2020_03_07_14_10_50.json | 105 ++++++++++++++ ...getMasterDatabase_2020_03_08_13_10_50.json | 105 ++++++++++++++ ...getMasterDatabase_2020_03_08_14_10_50.json | 105 ++++++++++++++ 9 files changed, 526 insertions(+), 18 deletions(-) create mode 100644 src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseServiceTest.java create mode 100644 src/test/java/de/deadlocker8/budgetmaster/unit/helpers/LoggerTestUtil.java create mode 100644 src/test/resources/backups/empty/.gitkeep create mode 100644 src/test/resources/backups/three/BudgetMasterDatabase_2020_03_07_14_10_50.json create mode 100644 src/test/resources/backups/three/BudgetMasterDatabase_2020_03_08_13_10_50.json create mode 100644 src/test/resources/backups/three/BudgetMasterDatabase_2020_03_08_14_10_50.json diff --git a/pom.xml b/pom.xml index d4d43580c..ee2d3f442 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,7 @@ <codemirror.version>5.45.0</codemirror.version> <webdrivermanager.version>2.2.1</webdrivermanager.version> <selenium.version>3.141.59</selenium.version> + <assertj-core.version>3.15.0</assertj-core.version> <app.versionDate>${maven.build.timestamp}</app.versionDate> <maven.build.timestamp.format>dd.MM.yy</maven.build.timestamp.format> @@ -215,6 +216,13 @@ <version>${selenium.version}</version> <scope>test</scope> </dependency> + + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <version>${assertj-core.version}</version> + <scope>test</scope> + </dependency> </dependencies> <build> diff --git a/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseService.java b/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseService.java index a130efc5f..a77ae72bb 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseService.java @@ -41,7 +41,7 @@ public class DatabaseService .registerTypeAdapter(DateTime.class, (JsonSerializer<DateTime>) (json, typeOfSrc, context) -> new JsonPrimitive(ISODateTimeFormat.date().print(json))) .create(); - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseService.class); private AccountService accountService; private CategoryService categoryService; private TransactionService transactionService; @@ -99,41 +99,55 @@ public class DatabaseService LOGGER.info("All tags reset."); } - private void rotatingBackup(Path backupFolderPath) + public void rotatingBackup(Path backupFolderPath) { + final List<Path> filesToDelete = determineFilesToDelete(backupFolderPath); + + for(Path path : filesToDelete) + { + try + { + Files.deleteIfExists(path); + } + catch(IOException e) + { + e.printStackTrace(); + } + } + } + + public List<Path> determineFilesToDelete(Path backupFolderPath) + { + final ArrayList<Path> filesToDelete = new ArrayList<>(); + final Integer numberOfFilesToKeep = settingsService.getSettings().getAutoBackupFilesToKeep(); if(numberOfFilesToKeep == 0) { LOGGER.debug("Skipping backup rotation since number of files to keep is set to unlimited"); - return; + return filesToDelete; } final List<String> existingBackups = getExistingBackups(backupFolderPath); if(existingBackups.size() < numberOfFilesToKeep) { LOGGER.debug("Skipping backup rotation (existing backups: " + existingBackups.size() + ", files to keep: " + numberOfFilesToKeep + ")"); - return; + return filesToDelete; } - LOGGER.debug("Removing old backups (existing backups: " + existingBackups.size() + ", files to keep: " + numberOfFilesToKeep + ")"); + LOGGER.debug("Determining old backups (existing backups: " + existingBackups.size() + ", files to keep: " + numberOfFilesToKeep + ")"); // reserve 1 file for the backup created afterwards final int allowedNumberOfFiles = existingBackups.size() - numberOfFilesToKeep + 1; for(int i = 0; i < allowedNumberOfFiles; i++) { final Path oldBackup = Paths.get(existingBackups.get(i)); - LOGGER.debug("Deleting old backup: " + oldBackup.toString()); - try - { - Files.deleteIfExists(oldBackup); - } - catch(IOException e) - { - e.printStackTrace(); - } + LOGGER.debug("Schedule old backup for deletion: " + oldBackup.toString()); + filesToDelete.add(oldBackup); } + + return filesToDelete; } - private List<String> getExistingBackups(Path backupFolderPath) + public List<String> getExistingBackups(Path backupFolderPath) { try(Stream<Path> walk = Files.walk(backupFolderPath)) { @@ -159,7 +173,7 @@ public class DatabaseService rotatingBackup(backupFolderPath); final Database databaseForJsonSerialization = getDatabaseForJsonSerialization(); - final String fileName = "BudgetMasterDatabase_" + DateTime.now().toString("yyyy_MM_dd_HH_mm_ss") + ".json"; + final String fileName = getExportFileName(true); final String backupPath = backupFolderPath.resolve(fileName).toString(); try(Writer writer = new FileWriter(backupPath)) @@ -174,6 +188,17 @@ public class DatabaseService } } + public static String getExportFileName(boolean includeTime) + { + String formatString = "yyyy_MM_dd"; + if(includeTime) + { + formatString = "yyyy_MM_dd_HH_mm_ss"; + } + + return "BudgetMasterDatabase_" + DateTime.now().toString(formatString) + ".json"; + } + public Database getDatabaseForJsonSerialization() { List<Category> categories = categoryService.getRepository().findAll(); diff --git a/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java b/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java index ce9228c4c..3baf34fc4 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java @@ -19,7 +19,6 @@ import de.deadlocker8.budgetmaster.utils.Strings; import de.thecodelabs.utils.util.Localization; import de.thecodelabs.utils.util.RandomUtils; import de.thecodelabs.versionizer.UpdateItem; -import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -41,6 +40,7 @@ import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -114,6 +114,12 @@ public class SettingsController extends BaseController settings.setAutoBackupTime(defaultSettings.getAutoBackupTime()); settings.setAutoBackupFilesToKeep(defaultSettings.getAutoBackupFilesToKeep()); } + else + { + final Path applicationSupportFolder = Main.getApplicationSupportFolder(); + final Path backupFolder = applicationSupportFolder.resolve("backups"); + databaseService.backupDatabase(backupFolder); + } if(bindingResult.hasErrors()) { @@ -173,7 +179,7 @@ public class SettingsController extends BaseController String data = DatabaseService.GSON.toJson(databaseForJsonSerialization); byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8); - String fileName = "BudgetMasterDatabase_" + DateTime.now().toString("yyyy_MM_dd") + ".json"; + String fileName = DatabaseService.getExportFileName(false); response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); response.setContentType("application/json; charset=UTF-8"); diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseServiceTest.java b/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseServiceTest.java new file mode 100644 index 000000000..869248796 --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseServiceTest.java @@ -0,0 +1,133 @@ +package de.deadlocker8.budgetmaster.unit.database; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import de.deadlocker8.budgetmaster.accounts.AccountService; +import de.deadlocker8.budgetmaster.categories.CategoryService; +import de.deadlocker8.budgetmaster.database.DatabaseService; +import de.deadlocker8.budgetmaster.settings.Settings; +import de.deadlocker8.budgetmaster.settings.SettingsService; +import de.deadlocker8.budgetmaster.tags.TagService; +import de.deadlocker8.budgetmaster.transactions.TransactionService; +import de.deadlocker8.budgetmaster.unit.helpers.LoggerTestUtil; +import org.assertj.core.groups.Tuple; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + + +@RunWith(SpringJUnit4ClassRunner.class) +public class DatabaseServiceTest +{ + @Mock + private AccountService accountService; + + @Mock + private SettingsService settingsService; + + @Mock + private CategoryService categoryService; + + @Mock + private TransactionService transactionService; + + @Mock + private TagService tagService; + + @InjectMocks + private DatabaseService databaseService; + + @Test + public void test_determineFilesToDelete_unlimited() throws URISyntaxException + { + ListAppender<ILoggingEvent> loggingAppender = LoggerTestUtil.getListAppenderForClass(DatabaseService.class); + + final Settings mockedSettings = Settings.getDefault(); + mockedSettings.setAutoBackupFilesToKeep(0); + when(settingsService.getSettings()).thenReturn(mockedSettings); + + final Path backupFolder = Paths.get(getClass().getClassLoader().getResource("").toURI()).resolve("backups/empty"); + final List<Path> filesToDelete = databaseService.determineFilesToDelete(backupFolder); + + assertThat(filesToDelete).isEmpty(); + + assertThat(loggingAppender.list) + .extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel) + .contains(Tuple.tuple("Skipping backup rotation since number of files to keep is set to unlimited", Level.DEBUG)); + } + + @Test + public void test_determineFilesToDelete_limitNotReached() throws URISyntaxException + { + ListAppender<ILoggingEvent> loggingAppender = LoggerTestUtil.getListAppenderForClass(DatabaseService.class); + + final Settings mockedSettings = Settings.getDefault(); + mockedSettings.setAutoBackupFilesToKeep(5); + when(settingsService.getSettings()).thenReturn(mockedSettings); + + final Path backupFolder = Paths.get(getClass().getClassLoader().getResource("").toURI()).resolve("backups/three"); + final List<Path> filesToDelete = databaseService.determineFilesToDelete(backupFolder); + + assertThat(filesToDelete).isEmpty(); + + assertThat(loggingAppender.list) + .extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel) + .contains(Tuple.tuple("Skipping backup rotation (existing backups: 3, files to keep: 5)", Level.DEBUG)); + } + + @Test + public void test_determineFilesToDelete_limitReachedExactly() throws URISyntaxException + { + ListAppender<ILoggingEvent> loggingAppender = LoggerTestUtil.getListAppenderForClass(DatabaseService.class); + + final Settings mockedSettings = Settings.getDefault(); + mockedSettings.setAutoBackupFilesToKeep(3); + when(settingsService.getSettings()).thenReturn(mockedSettings); + + final Path testResources = Paths.get(getClass().getClassLoader().getResource("").toURI()); + final Path backupFolder = testResources.resolve("backups/three"); + final List<Path> filesToDelete = databaseService.determineFilesToDelete(backupFolder); + + assertThat(filesToDelete) + .hasSize(1) + .contains(testResources.resolve("backups/three/BudgetMasterDatabase_2020_03_07_14_10_50.json")); + + assertThat(loggingAppender.list) + .extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel) + .contains(Tuple.tuple("Determining old backups (existing backups: 3, files to keep: 3)", Level.DEBUG)); + } + + @Test + public void test_determineFilesToDelete_limitReached() throws URISyntaxException + { + ListAppender<ILoggingEvent> loggingAppender = LoggerTestUtil.getListAppenderForClass(DatabaseService.class); + + final Settings mockedSettings = Settings.getDefault(); + mockedSettings.setAutoBackupFilesToKeep(2); + when(settingsService.getSettings()).thenReturn(mockedSettings); + + final Path testResources = Paths.get(getClass().getClassLoader().getResource("").toURI()); + final Path backupFolder = testResources.resolve("backups/three"); + final List<Path> filesToDelete = databaseService.determineFilesToDelete(backupFolder); + + assertThat(filesToDelete) + .hasSize(2) + .contains(testResources.resolve("backups/three/BudgetMasterDatabase_2020_03_07_14_10_50.json")) + .contains(testResources.resolve("backups/three/BudgetMasterDatabase_2020_03_08_13_10_50.json")); + + assertThat(loggingAppender.list) + .extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel) + .contains(Tuple.tuple("Determining old backups (existing backups: 3, files to keep: 2)", Level.DEBUG)); + } +} \ No newline at end of file diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/helpers/LoggerTestUtil.java b/src/test/java/de/deadlocker8/budgetmaster/unit/helpers/LoggerTestUtil.java new file mode 100644 index 000000000..f74212583 --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/unit/helpers/LoggerTestUtil.java @@ -0,0 +1,21 @@ +package de.deadlocker8.budgetmaster.unit.helpers; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import org.slf4j.LoggerFactory; + +public class LoggerTestUtil +{ + public static ListAppender<ILoggingEvent> getListAppenderForClass(Class clazz) + { + Logger logger = (Logger) LoggerFactory.getLogger(clazz); + + ListAppender<ILoggingEvent> loggingEventListAppender = new ListAppender<>(); + loggingEventListAppender.start(); + + logger.addAppender(loggingEventListAppender); + + return loggingEventListAppender; + } +} diff --git a/src/test/resources/backups/empty/.gitkeep b/src/test/resources/backups/empty/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/resources/backups/three/BudgetMasterDatabase_2020_03_07_14_10_50.json b/src/test/resources/backups/three/BudgetMasterDatabase_2020_03_07_14_10_50.json new file mode 100644 index 000000000..f976e90dd --- /dev/null +++ b/src/test/resources/backups/three/BudgetMasterDatabase_2020_03_07_14_10_50.json @@ -0,0 +1,105 @@ +{ + "TYPE": "BUDGETMASTER_DATABASE", + "VERSION": 3, + "categories": [ + { + "ID": 1, + "name": "No Category", + "color": "#eeeeee", + "type": "NONE" + }, + { + "ID": 2, + "name": "Rest", + "color": "#FFFF00", + "type": "REST" + }, + { + "ID": 39, + "name": "dfgdg", + "color": "#ff9500", + "type": "CUSTOM" + } + ], + "accounts": [ + { + "ID": 1, + "name": "Placeholder", + "type": "ALL" + }, + { + "ID": 2, + "name": "fdsdasa", + "type": "CUSTOM" + }, + { + "ID": 37, + "name": "d2", + "type": "CUSTOM" + } + ], + "transactions": [ + { + "ID": 37, + "amount": -100, + "date": "2020-02-05", + "account": { + "ID": 2, + "name": "fdsdasa", + "type": "CUSTOM" + }, + "category": { + "ID": 39, + "name": "dfgdg", + "color": "#ff9500", + "type": "CUSTOM" + }, + "name": "Apfel", + "description": "", + "tags": [] + }, + { + "ID": 38, + "amount": -200, + "date": "2020-02-05", + "account": { + "ID": 2, + "name": "fdsdasa", + "type": "CUSTOM" + }, + "category": { + "ID": 1, + "name": "No Category", + "color": "#eeeeee", + "type": "NONE" + }, + "name": "Birne", + "description": "", + "tags": [] + }, + { + "ID": 39, + "amount": -100, + "date": "2020-02-26", + "account": { + "ID": 2, + "name": "fdsdasa", + "type": "CUSTOM" + }, + "category": { + "ID": 1, + "name": "No Category", + "color": "#eeeeee", + "type": "NONE" + }, + "name": "Transfer Me", + "description": "", + "tags": [], + "transferAccount": { + "ID": 37, + "name": "d2", + "type": "CUSTOM" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/backups/three/BudgetMasterDatabase_2020_03_08_13_10_50.json b/src/test/resources/backups/three/BudgetMasterDatabase_2020_03_08_13_10_50.json new file mode 100644 index 000000000..f976e90dd --- /dev/null +++ b/src/test/resources/backups/three/BudgetMasterDatabase_2020_03_08_13_10_50.json @@ -0,0 +1,105 @@ +{ + "TYPE": "BUDGETMASTER_DATABASE", + "VERSION": 3, + "categories": [ + { + "ID": 1, + "name": "No Category", + "color": "#eeeeee", + "type": "NONE" + }, + { + "ID": 2, + "name": "Rest", + "color": "#FFFF00", + "type": "REST" + }, + { + "ID": 39, + "name": "dfgdg", + "color": "#ff9500", + "type": "CUSTOM" + } + ], + "accounts": [ + { + "ID": 1, + "name": "Placeholder", + "type": "ALL" + }, + { + "ID": 2, + "name": "fdsdasa", + "type": "CUSTOM" + }, + { + "ID": 37, + "name": "d2", + "type": "CUSTOM" + } + ], + "transactions": [ + { + "ID": 37, + "amount": -100, + "date": "2020-02-05", + "account": { + "ID": 2, + "name": "fdsdasa", + "type": "CUSTOM" + }, + "category": { + "ID": 39, + "name": "dfgdg", + "color": "#ff9500", + "type": "CUSTOM" + }, + "name": "Apfel", + "description": "", + "tags": [] + }, + { + "ID": 38, + "amount": -200, + "date": "2020-02-05", + "account": { + "ID": 2, + "name": "fdsdasa", + "type": "CUSTOM" + }, + "category": { + "ID": 1, + "name": "No Category", + "color": "#eeeeee", + "type": "NONE" + }, + "name": "Birne", + "description": "", + "tags": [] + }, + { + "ID": 39, + "amount": -100, + "date": "2020-02-26", + "account": { + "ID": 2, + "name": "fdsdasa", + "type": "CUSTOM" + }, + "category": { + "ID": 1, + "name": "No Category", + "color": "#eeeeee", + "type": "NONE" + }, + "name": "Transfer Me", + "description": "", + "tags": [], + "transferAccount": { + "ID": 37, + "name": "d2", + "type": "CUSTOM" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/backups/three/BudgetMasterDatabase_2020_03_08_14_10_50.json b/src/test/resources/backups/three/BudgetMasterDatabase_2020_03_08_14_10_50.json new file mode 100644 index 000000000..f976e90dd --- /dev/null +++ b/src/test/resources/backups/three/BudgetMasterDatabase_2020_03_08_14_10_50.json @@ -0,0 +1,105 @@ +{ + "TYPE": "BUDGETMASTER_DATABASE", + "VERSION": 3, + "categories": [ + { + "ID": 1, + "name": "No Category", + "color": "#eeeeee", + "type": "NONE" + }, + { + "ID": 2, + "name": "Rest", + "color": "#FFFF00", + "type": "REST" + }, + { + "ID": 39, + "name": "dfgdg", + "color": "#ff9500", + "type": "CUSTOM" + } + ], + "accounts": [ + { + "ID": 1, + "name": "Placeholder", + "type": "ALL" + }, + { + "ID": 2, + "name": "fdsdasa", + "type": "CUSTOM" + }, + { + "ID": 37, + "name": "d2", + "type": "CUSTOM" + } + ], + "transactions": [ + { + "ID": 37, + "amount": -100, + "date": "2020-02-05", + "account": { + "ID": 2, + "name": "fdsdasa", + "type": "CUSTOM" + }, + "category": { + "ID": 39, + "name": "dfgdg", + "color": "#ff9500", + "type": "CUSTOM" + }, + "name": "Apfel", + "description": "", + "tags": [] + }, + { + "ID": 38, + "amount": -200, + "date": "2020-02-05", + "account": { + "ID": 2, + "name": "fdsdasa", + "type": "CUSTOM" + }, + "category": { + "ID": 1, + "name": "No Category", + "color": "#eeeeee", + "type": "NONE" + }, + "name": "Birne", + "description": "", + "tags": [] + }, + { + "ID": 39, + "amount": -100, + "date": "2020-02-26", + "account": { + "ID": 2, + "name": "fdsdasa", + "type": "CUSTOM" + }, + "category": { + "ID": 1, + "name": "No Category", + "color": "#eeeeee", + "type": "NONE" + }, + "name": "Transfer Me", + "description": "", + "tags": [], + "transferAccount": { + "ID": 37, + "name": "d2", + "type": "CUSTOM" + } + } + ] +} \ No newline at end of file -- GitLab