From cd1acfde1b0074ca0621d3635ec358921da7d8e6 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 18 Jun 2016 11:13:17 +0200 Subject: [PATCH] #674 Create tests for purge commands and purge service --- .../fr/xephi/authme/task/PurgeService.java | 22 +- .../authme/PurgeBannedPlayersCommandTest.java | 74 +++++ .../executable/authme/PurgeCommandTest.java | 89 ++++++ .../xephi/authme/task/PurgeServiceTest.java | 273 ++++++++++++++++++ 4 files changed, 442 insertions(+), 16 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommandTest.java create mode 100644 src/test/java/fr/xephi/authme/command/executable/authme/PurgeCommandTest.java create mode 100644 src/test/java/fr/xephi/authme/task/PurgeServiceTest.java diff --git a/src/main/java/fr/xephi/authme/task/PurgeService.java b/src/main/java/fr/xephi/authme/task/PurgeService.java index a2de2cd7e..baa4e9eb9 100644 --- a/src/main/java/fr/xephi/authme/task/PurgeService.java +++ b/src/main/java/fr/xephi/authme/task/PurgeService.java @@ -48,11 +48,6 @@ public class PurgeService implements Reloadable { private boolean isPurging = false; // Settings - private boolean removeEssentialsFiles; - private boolean removePlayerDat; - private boolean removeLimitedCreativeInventories; - private boolean removeAntiXrayFiles; - private boolean removePermissions; private int daysBeforePurge; /** @@ -86,7 +81,7 @@ public class PurgeService implements Reloadable { ConsoleLogger.info("Automatically purging the database..."); Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.DATE, daysBeforePurge); + calendar.add(Calendar.DATE, -daysBeforePurge); long until = calendar.getTimeInMillis(); runPurge(null, until); @@ -154,7 +149,7 @@ public class PurgeService implements Reloadable { } synchronized void purgeAntiXray(Set cleared) { - if (!removeAntiXrayFiles) { + if (!settings.getProperty(PurgeSettings.REMOVE_ANTI_XRAY_FILE)) { return; } @@ -178,7 +173,7 @@ public class PurgeService implements Reloadable { } synchronized void purgeLimitedCreative(Set cleared) { - if (!removeLimitedCreativeInventories) { + if (!settings.getProperty(PurgeSettings.REMOVE_LIMITED_CREATIVE_INVENTORIES)) { return; } @@ -219,7 +214,7 @@ public class PurgeService implements Reloadable { } synchronized void purgeDat(Set cleared) { - if (!removePlayerDat) { + if (!settings.getProperty(PurgeSettings.REMOVE_PLAYER_DAT)) { return; } @@ -243,7 +238,7 @@ public class PurgeService implements Reloadable { * @param cleared List of String */ synchronized void purgeEssentials(Set cleared) { - if (!removeEssentialsFiles && !pluginHooks.isEssentialsAvailable()) { + if (!settings.getProperty(PurgeSettings.REMOVE_ESSENTIALS_FILES) && !pluginHooks.isEssentialsAvailable()) { return; } @@ -272,7 +267,7 @@ public class PurgeService implements Reloadable { // TODO: What is this method for? Is it correct? // TODO: Make it work with OfflinePlayers group data. synchronized void purgePermissions(Set cleared) { - if (!removePermissions) { + if (!settings.getProperty(PurgeSettings.REMOVE_PERMISSIONS)) { return; } @@ -294,11 +289,6 @@ public class PurgeService implements Reloadable { @PostConstruct @Override public void reload() { - this.removeEssentialsFiles = settings.getProperty(PurgeSettings.REMOVE_ESSENTIALS_FILES); - this.removePlayerDat = settings.getProperty(PurgeSettings.REMOVE_PLAYER_DAT); - this.removeAntiXrayFiles = settings.getProperty(PurgeSettings.REMOVE_ANTI_XRAY_FILE); - this.removeLimitedCreativeInventories = settings.getProperty(PurgeSettings.REMOVE_LIMITED_CREATIVE_INVENTORIES); - this.removePermissions = settings.getProperty(PurgeSettings.REMOVE_PERMISSIONS); this.daysBeforePurge = settings.getProperty(PurgeSettings.DAYS_BEFORE_REMOVE_PLAYER); } } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommandTest.java new file mode 100644 index 000000000..295186cfe --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommandTest.java @@ -0,0 +1,74 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.task.PurgeService; +import fr.xephi.authme.util.BukkitService; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static com.google.common.collect.Sets.newHashSet; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link PurgeBannedPlayersCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class PurgeBannedPlayersCommandTest { + + @InjectMocks + private PurgeBannedPlayersCommand command; + + @Mock + private PurgeService purgeService; + + @Mock + private BukkitService bukkitService; + + @Test + public void shouldForwardRequestToService() { + // given + String[] names = {"bannedPlayer", "other_banned", "evilplayer", "Someone"}; + OfflinePlayer[] players = offlinePlayersWithNames(names); + given(bukkitService.getBannedPlayers()).willReturn(newHashSet(players)); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.emptyList()); + + // then + verify(bukkitService).getBannedPlayers(); + verify(purgeService).purgePlayers(eq(sender), eq(asLowerCaseSet(names)), + argThat(arrayContainingInAnyOrder(players))); + } + + private static OfflinePlayer[] offlinePlayersWithNames(String... names) { + OfflinePlayer[] players = new OfflinePlayer[names.length]; + for (int i = 0; i < names.length; ++i) { + OfflinePlayer player = mock(OfflinePlayer.class); + given(player.getName()).willReturn(names[i]); + players[i] = player; + } + return players; + } + + private static Set asLowerCaseSet(String... items) { + Set result = new HashSet<>(items.length); + for (String item : items) { + result.add(item.toLowerCase()); + } + return result; + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/PurgeCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/PurgeCommandTest.java new file mode 100644 index 000000000..05074adf6 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/PurgeCommandTest.java @@ -0,0 +1,89 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.task.PurgeService; +import org.bukkit.command.CommandSender; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Calendar; +import java.util.Collections; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link PurgeCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class PurgeCommandTest { + + @InjectMocks + private PurgeCommand command; + + @Mock + private PurgeService purgeService; + + @Test + public void shouldHandleInvalidNumber() { + // given + String interval = "invalid"; + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.singletonList(interval)); + + // then + verify(sender).sendMessage(argThat(containsString("The value you've entered is invalid"))); + verifyZeroInteractions(purgeService); + } + + @Test + public void shouldRejectTooSmallInterval() { + // given + String interval = "29"; + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.singletonList(interval)); + + // then + verify(sender).sendMessage(argThat(containsString("You can only purge data older than 30 days"))); + verifyZeroInteractions(purgeService); + } + + @Test + public void shouldForwardToService() { + // given + String interval = "45"; + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.singletonList(interval)); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(Long.class); + verify(purgeService).runPurge(eq(sender), captor.capture()); + + // Check the timestamp with a certain tolerance + int toleranceMillis = 100; + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DATE, -Integer.valueOf(interval)); + assertIsCloseTo(captor.getValue(), calendar.getTimeInMillis(), toleranceMillis); + } + + private static void assertIsCloseTo(long value1, long value2, long tolerance) { + assertThat(Math.abs(value1 - value2), not(greaterThan(tolerance))); + } + +} diff --git a/src/test/java/fr/xephi/authme/task/PurgeServiceTest.java b/src/test/java/fr/xephi/authme/task/PurgeServiceTest.java new file mode 100644 index 000000000..b20df3258 --- /dev/null +++ b/src/test/java/fr/xephi/authme/task/PurgeServiceTest.java @@ -0,0 +1,273 @@ +package fr.xephi.authme.task; + +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.hooks.PluginHooks; +import fr.xephi.authme.initialization.FieldInjection; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.PurgeSettings; +import fr.xephi.authme.util.BukkitService; +import org.bukkit.OfflinePlayer; +import org.bukkit.Server; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static com.google.common.collect.Sets.newHashSet; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anySet; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link PurgeService}. + */ +@RunWith(MockitoJUnitRunner.class) +public class PurgeServiceTest { + + private PurgeService purgeService; + + @Mock + private BukkitService bukkitService; + @Mock + private DataSource dataSource; + @Mock + private NewSetting settings; + @Mock + private PermissionsManager permissionsManager; + @Mock + private PluginHooks pluginHooks; + @Mock + private Server server; + + @BeforeClass + public static void initLogger() { + TestHelper.setupLogger(); + } + + @Before + public void initSettingDefaults() { + given(settings.getProperty(PurgeSettings.DAYS_BEFORE_REMOVE_PLAYER)).willReturn(60); + } + + @Test + public void shouldNotRunAutoPurge() { + // given + given(settings.getProperty(PurgeSettings.USE_AUTO_PURGE)).willReturn(false); + + // when + initPurgeService(); + purgeService.runAutoPurge(); + + // then + verifyZeroInteractions(bukkitService, dataSource); + } + + @Test + public void shouldNotRunAutoPurgeForInvalidInterval() { + // given + given(settings.getProperty(PurgeSettings.USE_AUTO_PURGE)).willReturn(true); + given(settings.getProperty(PurgeSettings.DAYS_BEFORE_REMOVE_PLAYER)).willReturn(0); + + // when + initPurgeService(); + purgeService.runAutoPurge(); + + // then + verifyZeroInteractions(bukkitService, dataSource); + } + + @Test + public void shouldRunAutoPurge() { + // given + given(settings.getProperty(PurgeSettings.USE_AUTO_PURGE)).willReturn(true); + given(settings.getProperty(PurgeSettings.DAYS_BEFORE_REMOVE_PLAYER)).willReturn(60); + String[] playerNames = {"alpha", "bravo", "charlie", "delta"}; + given(dataSource.getRecordsToPurge(anyLong())).willReturn(newHashSet(playerNames)); + mockReturnedOfflinePlayers(); + mockHasBypassPurgePermission("bravo", "delta"); + + // when + initPurgeService(); + purgeService.runAutoPurge(); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(Long.class); + verify(dataSource).getRecordsToPurge(captor.capture()); + assertCorrectPurgeTimestamp(captor.getValue(), 60); + verify(dataSource).purgeRecords(newHashSet("alpha", "charlie")); + assertThat(purgeService.isPurging(), equalTo(true)); + verifyScheduledPurgeTask(null, "alpha", "charlie"); + } + + @Test + public void shouldRecognizeNoPlayersToPurge() { + // given + long delay = 123012301L; + given(dataSource.getRecordsToPurge(delay)).willReturn(Collections.emptySet()); + CommandSender sender = mock(CommandSender.class); + + // when + initPurgeService(); + purgeService.runPurge(sender, delay); + + // then + verify(dataSource).getRecordsToPurge(delay); + verify(dataSource, never()).purgeRecords(anySet()); + verify(sender).sendMessage("No players to purge"); + verifyZeroInteractions(bukkitService, permissionsManager); + } + + @Test + public void shouldRunPurge() { + // given + long delay = 1809714L; + given(dataSource.getRecordsToPurge(delay)).willReturn(newHashSet("charlie", "delta", "echo", "foxtrot")); + mockReturnedOfflinePlayers(); + mockHasBypassPurgePermission("echo"); + Player sender = mock(Player.class); + UUID uuid = UUID.randomUUID(); + given(sender.getUniqueId()).willReturn(uuid); + + // when + initPurgeService(); + purgeService.runPurge(sender, delay); + + // then + verify(dataSource).getRecordsToPurge(delay); + verify(dataSource).purgeRecords(newHashSet("charlie", "delta", "foxtrot")); + verify(sender).sendMessage(argThat(containsString("Deleted 3 user accounts"))); + verifyScheduledPurgeTask(uuid, "charlie", "delta", "foxtrot"); + } + + @Test + public void shouldRunPurgeIfProcessIsAlreadyRunning() { + // given + initPurgeService(); + purgeService.setPurging(true); + CommandSender sender = mock(CommandSender.class); + OfflinePlayer[] players = mockReturnedOfflinePlayers(); + + // when + purgeService.purgePlayers(sender, newHashSet("test", "names"), players); + + // then + verify(sender).sendMessage(argThat(containsString("Purge is already in progress"))); + verifyZeroInteractions(bukkitService, dataSource, permissionsManager); + } + + /** + * Returns mock OfflinePlayer objects with names corresponding to A - G of the NATO phonetic alphabet, + * in various casing. + * + * @return list of offline players BukkitService is mocked to return + */ + private OfflinePlayer[] mockReturnedOfflinePlayers() { + String[] names = { "alfa", "Bravo", "charLIE", "delta", "ECHO", "Foxtrot", "golf" }; + OfflinePlayer[] players = new OfflinePlayer[names.length]; + for (int i = 0; i < names.length; ++i) { + OfflinePlayer player = mock(OfflinePlayer.class); + given(player.getName()).willReturn(names[i]); + players[i] = player; + } + given(bukkitService.getOfflinePlayers()).willReturn(players); + return players; + } + + /** + * Mocks the permission manager to say that the given names have the bypass purge permission. + * + * @param names the names + */ + private void mockHasBypassPurgePermission(String... names) { + for (String name : names) { + given(permissionsManager.hasPermissionOffline( + argThat(equalToIgnoringCase(name)), eq(PlayerStatePermission.BYPASS_PURGE))).willReturn(true); + } + } + + private void assertCorrectPurgeTimestamp(long timestamp, int configuredDays) { + final long toleranceMillis = 100L; + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, -configuredDays); + long expectedTimestamp = cal.getTimeInMillis(); + + assertThat("Timestamp is equal to now minus " + configuredDays + " days (within tolerance)", + Math.abs(timestamp - expectedTimestamp), not(greaterThan(toleranceMillis))); + } + + @SuppressWarnings("unchecked") + private void verifyScheduledPurgeTask(UUID uuid, String... names) { + ArgumentCaptor captor = ArgumentCaptor.forClass(PurgeTask.class); + verify(bukkitService).runTaskAsynchronously(captor.capture()); + PurgeTask task = captor.getValue(); + + Object senderInTask = ReflectionTestUtils.getFieldValue(PurgeTask.class, task, "sender"); + Set namesInTask = (Set) ReflectionTestUtils.getFieldValue(PurgeTask.class, task, "toPurge"); + assertThat(senderInTask, Matchers.equalTo(uuid)); + assertThat(namesInTask, containsInAnyOrder(names)); + } + + // --------------- + // TODO ljacqu 20160618: Create a delayed injection test runner instead + private void initPurgeService() { + FieldInjection injection = FieldInjection.provide(PurgeService.class).get(); + purgeService = injection.instantiateWith(getFields(injection.getDependencies())); + purgeService.reload(); // because annotated with @PostConstruct + } + + private Object[] getFields(Class[] classes) { + Map, Object> mocksByType = orderMocksByType(); + List orderedMocks = new ArrayList<>(classes.length); + for (Class clazz : classes) { + orderedMocks.add(mocksByType.get(clazz)); + } + return orderedMocks.toArray(); + } + + private Map, Object> orderMocksByType() { + Map, Object> mocksByType = new HashMap<>(); + for (Field field : PurgeServiceTest.class.getDeclaredFields()) { + if (field.isAnnotationPresent(Mock.class)) { + try { + mocksByType.put(field.getType(), field.get(this)); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + } + return mocksByType; + } +}