diff --git a/src/main/java/fr/xephi/authme/AntiBot.java b/src/main/java/fr/xephi/authme/AntiBot.java index 00a53daf3..75a2a3b59 100644 --- a/src/main/java/fr/xephi/authme/AntiBot.java +++ b/src/main/java/fr/xephi/authme/AntiBot.java @@ -4,26 +4,32 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.ProtectionSettings; import fr.xephi.authme.util.BukkitService; -import org.bukkit.Bukkit; import org.bukkit.entity.Player; import java.util.ArrayList; import java.util.List; +import static fr.xephi.authme.util.BukkitService.TICKS_PER_MINUTE; +import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; + /** * The AntiBot Service Management class. */ public class AntiBot { + private final NewSetting settings; private final Messages messages; private final PermissionsManager permissionsManager; private final BukkitService bukkitService; private final List antibotPlayers = new ArrayList<>(); private AntiBotStatus antiBotStatus = AntiBotStatus.DISABLED; - public AntiBot(Messages messages, PermissionsManager permissionsManager, BukkitService bukkitService) { + public AntiBot(NewSetting settings, Messages messages, PermissionsManager permissionsManager, + BukkitService bukkitService) { + this.settings = settings; this.messages = messages; this.permissionsManager = permissionsManager; this.bukkitService = bukkitService; @@ -32,15 +38,14 @@ public class AntiBot { } private void setupAntiBotService() { - if (!Settings.enableAntiBot) { - return; + if (settings.getProperty(ProtectionSettings.ENABLE_ANTIBOT)) { + bukkitService.scheduleSyncDelayedTask(new Runnable() { + @Override + public void run() { + antiBotStatus = AntiBotStatus.LISTENING; + } + }, 2 * TICKS_PER_MINUTE); } - bukkitService.scheduleSyncDelayedTask(new Runnable() { - @Override - public void run() { - antiBotStatus = AntiBotStatus.LISTENING; - } - }, 2400); } public void overrideAntiBotStatus(boolean activated) { @@ -60,9 +65,10 @@ public class AntiBot { public void activateAntiBot() { antiBotStatus = AntiBotStatus.ACTIVE; for (String s : messages.retrieve(MessageKey.ANTIBOT_AUTO_ENABLED_MESSAGE)) { - Bukkit.broadcastMessage(s); + bukkitService.broadcastMessage(s); } + final int duration = settings.getProperty(ProtectionSettings.ANTIBOT_DURATION); bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { @@ -70,11 +76,11 @@ public class AntiBot { antiBotStatus = AntiBotStatus.LISTENING; antibotPlayers.clear(); for (String s : messages.retrieve(MessageKey.ANTIBOT_AUTO_DISABLED_MESSAGE)) { - bukkitService.broadcastMessage(s.replace("%m", Integer.toString(Settings.antiBotDuration))); + bukkitService.broadcastMessage(s.replace("%m", Integer.toString(duration))); } } } - }, Settings.antiBotDuration * 1200); + }, duration * TICKS_PER_MINUTE); } public void checkAntiBot(final Player player) { @@ -86,7 +92,7 @@ public class AntiBot { } antibotPlayers.add(player.getName().toLowerCase()); - if (antibotPlayers.size() > Settings.antiBotSensibility) { + if (antibotPlayers.size() > settings.getProperty(ProtectionSettings.ANTIBOT_SENSIBILITY)) { activateAntiBot(); return; } @@ -95,7 +101,7 @@ public class AntiBot { public void run() { antibotPlayers.remove(player.getName().toLowerCase()); } - }, 300); + }, 15 * TICKS_PER_SECOND); } public enum AntiBotStatus { diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 27eafb40c..de175eb12 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -258,7 +258,7 @@ public class AuthMe extends JavaPlugin { // AntiBot delay BukkitService bukkitService = new BukkitService(this); - antiBot = new AntiBot(messages, permsMan, bukkitService); + antiBot = new AntiBot(newSettings, messages, permsMan, bukkitService); // Set up the permissions manager and command handler permsMan = initializePermissionsManager(); diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index fdf77375f..5989e2165 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -54,7 +54,7 @@ public final class Settings { emailRegistration, multiverse, bungee, banUnsafeIp, doubleEmailCheck, sessionExpireOnIpChange, disableSocialSpy, useEssentialsMotd, - enableProtection, enableAntiBot, recallEmail, useWelcomeMessage, + enableProtection, recallEmail, useWelcomeMessage, broadcastWelcomeMessage, forceRegKick, forceRegLogin, checkVeryGames, removeJoinMessage, removeLeaveMessage, delayJoinMessage, noTeleport, hideTablistBeforeLogin, denyTabcompleteBeforeLogin, @@ -70,9 +70,7 @@ public final class Settings { getPasswordMinLen, getMovementRadius, getmaxRegPerIp, getNonActivatedGroup, passwordMaxLength, getRecoveryPassLength, getMailPort, maxLoginTry, captchaLength, saltLength, - getmaxRegPerEmail, bCryptLog2Rounds, - antiBotSensibility, antiBotDuration, getMaxLoginPerIp, - getMaxJoinPerIp; + getmaxRegPerEmail, bCryptLog2Rounds, getMaxLoginPerIp, getMaxJoinPerIp; protected static FileConfiguration configFile; /** @@ -172,9 +170,6 @@ public final class Settings { defaultWorld = configFile.getString("Purge.defaultWorld", "world"); enableProtection = configFile.getBoolean("Protection.enableProtection", false); countries = configFile.getStringList("Protection.countries"); - enableAntiBot = configFile.getBoolean("Protection.enableAntiBot", false); - antiBotSensibility = configFile.getInt("Protection.antiBotSensibility", 5); - antiBotDuration = configFile.getInt("Protection.antiBotDuration", 10); forceCommands = configFile.getStringList("settings.forceCommands"); forceCommandsAsConsole = configFile.getStringList("settings.forceCommandsAsConsole"); recallEmail = configFile.getBoolean("Email.recallPlayers", false); diff --git a/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java b/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java index d4b5f00fc..a4792f91d 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java @@ -30,7 +30,7 @@ public class ProtectionSettings implements SettingsClass { public static final Property ENABLE_ANTIBOT = newProperty("Protection.enableAntiBot", false); - @Comment("Max number of player allowed to login in 5 secs before enable AntiBot system automatically") + @Comment("Max number of players allowed to login in 5 secs before the AntiBot system is enabled automatically") public static final Property ANTIBOT_SENSIBILITY = newProperty("Protection.antiBotSensibility", 5); diff --git a/src/main/java/fr/xephi/authme/util/BukkitService.java b/src/main/java/fr/xephi/authme/util/BukkitService.java index 41622413a..053fe7891 100644 --- a/src/main/java/fr/xephi/authme/util/BukkitService.java +++ b/src/main/java/fr/xephi/authme/util/BukkitService.java @@ -8,6 +8,11 @@ import org.bukkit.Bukkit; */ public class BukkitService { + /** Number of ticks per second in the Bukkit main thread. */ + public static final int TICKS_PER_SECOND = 20; + /** Number of ticks per minute. */ + public static final int TICKS_PER_MINUTE = 60 * TICKS_PER_SECOND; + private final AuthMe authMe; public BukkitService(AuthMe authMe) { diff --git a/src/test/java/fr/xephi/authme/AntiBotTest.java b/src/test/java/fr/xephi/authme/AntiBotTest.java new file mode 100644 index 000000000..4dace2944 --- /dev/null +++ b/src/test/java/fr/xephi/authme/AntiBotTest.java @@ -0,0 +1,212 @@ +package fr.xephi.authme; + +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.output.Messages; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.ProtectionSettings; +import fr.xephi.authme.util.BukkitService; +import org.bukkit.entity.Player; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.List; + +import static fr.xephi.authme.util.BukkitService.TICKS_PER_MINUTE; +import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link AntiBot}. + */ +@RunWith(MockitoJUnitRunner.class) +public class AntiBotTest { + + @Mock + private NewSetting settings; + @Mock + private Messages messages; + @Mock + private PermissionsManager permissionsManager; + @Mock + private BukkitService bukkitService; + + @Before + public void setDefaultSettingValues() { + given(settings.getProperty(ProtectionSettings.ENABLE_ANTIBOT)).willReturn(true); + } + + @Test + public void shouldKeepAntiBotDisabled() { + // given / when + given(settings.getProperty(ProtectionSettings.ENABLE_ANTIBOT)).willReturn(false); + AntiBot antiBot = new AntiBot(settings, messages, permissionsManager, bukkitService); + + // then + verify(bukkitService, never()).scheduleSyncDelayedTask(any(Runnable.class), anyLong()); + assertThat(antiBot.getAntiBotStatus(), equalTo(AntiBot.AntiBotStatus.DISABLED)); + } + + @Test + public void shouldTransitionToListening() { + // given / when + AntiBot antiBot = new AntiBot(settings, messages, permissionsManager, bukkitService); + TestHelper.runSyncDelayedTaskWithDelay(bukkitService); + + // then + assertThat(antiBot.getAntiBotStatus(), equalTo(AntiBot.AntiBotStatus.LISTENING)); + } + + @Test + public void shouldSetStatusToActive() { + // given + AntiBot antiBot = createListeningAntiBot(); + + // when + antiBot.overrideAntiBotStatus(true); + + // then + assertThat(antiBot.getAntiBotStatus(), equalTo(AntiBot.AntiBotStatus.ACTIVE)); + } + + @Test + public void shouldSetStatusToListening() { + // given + AntiBot antiBot = createListeningAntiBot(); + + // when + antiBot.overrideAntiBotStatus(false); + + // then + assertThat(antiBot.getAntiBotStatus(), equalTo(AntiBot.AntiBotStatus.LISTENING)); + } + + @Test + public void shouldRemainDisabled() { + // given + given(settings.getProperty(ProtectionSettings.ENABLE_ANTIBOT)).willReturn(false); + AntiBot antiBot = new AntiBot(settings, messages, permissionsManager, bukkitService); + + // when + antiBot.overrideAntiBotStatus(true); + + // then + assertThat(antiBot.getAntiBotStatus(), equalTo(AntiBot.AntiBotStatus.DISABLED)); + } + + @Test + public void shouldActivateAntiBot() { + // given + given(messages.retrieve(MessageKey.ANTIBOT_AUTO_ENABLED_MESSAGE)) + .willReturn(new String[]{"Test line #1", "Test line #2"}); + int duration = 300; + given(settings.getProperty(ProtectionSettings.ANTIBOT_DURATION)).willReturn(duration); + AntiBot antiBot = createListeningAntiBot(); + + // when + antiBot.activateAntiBot(); + + // then + assertThat(antiBot.getAntiBotStatus(), equalTo(AntiBot.AntiBotStatus.ACTIVE)); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(bukkitService, times(2)).broadcastMessage(captor.capture()); + assertThat(captor.getAllValues(), contains("Test line #1", "Test line #2")); + long expectedTicks = duration * TICKS_PER_MINUTE; + verify(bukkitService).scheduleSyncDelayedTask(any(Runnable.class), eq(expectedTicks)); + } + + @Test + public void shouldDisableAntiBotAfterSetDuration() { + // given + given(messages.retrieve(MessageKey.ANTIBOT_AUTO_ENABLED_MESSAGE)).willReturn(new String[0]); + given(messages.retrieve(MessageKey.ANTIBOT_AUTO_DISABLED_MESSAGE)) + .willReturn(new String[]{"Disabled...", "Placeholder: %m."}); + given(settings.getProperty(ProtectionSettings.ANTIBOT_DURATION)).willReturn(4); + AntiBot antiBot = createListeningAntiBot(); + + // when + antiBot.activateAntiBot(); + TestHelper.runSyncDelayedTaskWithDelay(bukkitService); + + // then + assertThat(antiBot.getAntiBotStatus(), equalTo(AntiBot.AntiBotStatus.LISTENING)); + verify(bukkitService).scheduleSyncDelayedTask(any(Runnable.class), eq((long) 4800)); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(bukkitService, times(2)).broadcastMessage(captor.capture()); + assertThat(captor.getAllValues(), contains("Disabled...", "Placeholder: 4.")); + } + + @Test + public void shouldCheckPlayerAndRemoveHimLater() { + // given + Player player = mock(Player.class); + given(player.getName()).willReturn("Plaer"); + given(permissionsManager.hasPermission(player, PlayerStatePermission.BYPASS_ANTIBOT)).willReturn(false); + given(settings.getProperty(ProtectionSettings.ANTIBOT_SENSIBILITY)).willReturn(10); + AntiBot antiBot = createListeningAntiBot(); + + // when + antiBot.checkAntiBot(player); + + // then + List playerList = (List) ReflectionTestUtils + .getFieldValue(AntiBot.class, antiBot, "antibotPlayers"); + assertThat(playerList, hasSize(1)); + verify(bukkitService).scheduleSyncDelayedTask(any(Runnable.class), eq((long) 15 * TICKS_PER_SECOND)); + + // Follow-up: Check that player will be removed from list again by running the Runnable + // given (2) + // Add another player to the list + playerList.add("other_player"); + + // when (2) + TestHelper.runSyncDelayedTaskWithDelay(bukkitService); + + // then (2) + assertThat(playerList, contains("other_player")); + } + + @Test + public void shouldNotUpdateListForPlayerWithByPassPermission() { + // given + Player player = mock(Player.class); + given(permissionsManager.hasPermission(player, PlayerStatePermission.BYPASS_ANTIBOT)).willReturn(true); + given(settings.getProperty(ProtectionSettings.ANTIBOT_SENSIBILITY)).willReturn(3); + AntiBot antiBot = createListeningAntiBot(); + + // when + antiBot.checkAntiBot(player); + + // then + List playerList = (List) ReflectionTestUtils.getFieldValue(AntiBot.class, antiBot, "antibotPlayers"); + assertThat(playerList, empty()); + verify(bukkitService, never()).scheduleSyncDelayedTask(any(Runnable.class), anyLong()); + } + + private AntiBot createListeningAntiBot() { + AntiBot antiBot = new AntiBot(settings, messages, permissionsManager, bukkitService); + TestHelper.runSyncDelayedTaskWithDelay(bukkitService); + // Make BukkitService forget about all interactions up to here + reset(bukkitService); + return antiBot; + } + +} diff --git a/src/test/java/fr/xephi/authme/TestHelper.java b/src/test/java/fr/xephi/authme/TestHelper.java index 5a6a2c8bd..5f1db10d1 100644 --- a/src/test/java/fr/xephi/authme/TestHelper.java +++ b/src/test/java/fr/xephi/authme/TestHelper.java @@ -1,6 +1,7 @@ package fr.xephi.authme; import fr.xephi.authme.command.CommandService; +import fr.xephi.authme.util.BukkitService; import org.mockito.ArgumentCaptor; import java.io.File; @@ -8,6 +9,7 @@ import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; +import static org.mockito.Matchers.anyLong; import static org.mockito.Mockito.verify; /** @@ -66,4 +68,32 @@ public final class TestHelper { runnable.run(); } + /** + * Execute a {@link Runnable} passed to a mock's {@link BukkitService#scheduleSyncDelayedTask(Runnable)} method. + * Note that calling this method expects that there be a runnable sent to the method and will fail + * otherwise. + * + * @param service The mock service + */ + public static void runSyncDelayedTask(BukkitService service) { + ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + verify(service).scheduleSyncDelayedTask(captor.capture()); + Runnable runnable = captor.getValue(); + runnable.run(); + } + + /** + * Execute a {@link Runnable} passed to a mock's {@link BukkitService#scheduleSyncDelayedTask(Runnable, long)} + * method. Note that calling this method expects that there be a runnable sent to the method and will fail + * otherwise. + * + * @param service The mock service + */ + public static void runSyncDelayedTaskWithDelay(BukkitService service) { + ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + verify(service).scheduleSyncDelayedTask(captor.capture(), anyLong()); + Runnable runnable = captor.getValue(); + runnable.run(); + } + } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java new file mode 100644 index 000000000..3f9767f92 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java @@ -0,0 +1,99 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.AntiBot; +import fr.xephi.authme.command.CommandService; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.command.FoundCommandResult; +import fr.xephi.authme.command.help.HelpProvider; +import org.bukkit.command.CommandSender; +import org.junit.Test; + +import java.util.Collections; + +import static java.util.Arrays.asList; +import static org.hamcrest.Matchers.containsString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link SwitchAntiBotCommand}. + */ +public class SwitchAntiBotCommandTest { + + @Test + public void shouldReturnAntiBotState() { + // given + AntiBot antiBot = mock(AntiBot.class); + given(antiBot.getAntiBotStatus()).willReturn(AntiBot.AntiBotStatus.ACTIVE); + CommandService service = mock(CommandService.class); + given(service.getAntiBot()).willReturn(antiBot); + CommandSender sender = mock(CommandSender.class); + ExecutableCommand command = new SwitchAntiBotCommand(); + + // when + command.executeCommand(sender, Collections.emptyList(), service); + + // then + verify(sender).sendMessage(argThat(containsString("status: ACTIVE"))); + } + + @Test + public void shouldActivateAntiBot() { + // given + AntiBot antiBot = mock(AntiBot.class); + CommandService service = mock(CommandService.class); + given(service.getAntiBot()).willReturn(antiBot); + CommandSender sender = mock(CommandSender.class); + ExecutableCommand command = new SwitchAntiBotCommand(); + + // when + command.executeCommand(sender, Collections.singletonList("on"), service); + + // then + verify(antiBot).overrideAntiBotStatus(true); + verify(sender).sendMessage(argThat(containsString("enabled"))); + } + + @Test + public void shouldDeactivateAntiBot() { + // given + AntiBot antiBot = mock(AntiBot.class); + CommandService service = mock(CommandService.class); + given(service.getAntiBot()).willReturn(antiBot); + CommandSender sender = mock(CommandSender.class); + ExecutableCommand command = new SwitchAntiBotCommand(); + + // when + command.executeCommand(sender, Collections.singletonList("Off"), service); + + // then + verify(antiBot).overrideAntiBotStatus(false); + verify(sender).sendMessage(argThat(containsString("disabled"))); + } + + @Test + public void shouldShowHelpForUnknownState() { + // given + CommandSender sender = mock(CommandSender.class); + + AntiBot antiBot = mock(AntiBot.class); + FoundCommandResult foundCommandResult = mock(FoundCommandResult.class); + CommandService service = mock(CommandService.class); + given(service.getAntiBot()).willReturn(antiBot); + given(service.mapPartsToCommand(sender, asList("authme", "antibot"))).willReturn(foundCommandResult); + + ExecutableCommand command = new SwitchAntiBotCommand(); + + // when + command.executeCommand(sender, Collections.singletonList("wrong"), service); + + // then + verify(antiBot, never()).overrideAntiBotStatus(anyBoolean()); + verify(sender).sendMessage(argThat(containsString("Invalid"))); + verify(service).outputHelp(sender, foundCommandResult, HelpProvider.SHOW_ARGUMENTS); + } +}