From 8d64c0e5bf4c19b8b0da9716aea1ea5277265ec2 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 1 Oct 2016 00:48:23 +0200 Subject: [PATCH] #830 Initialize login process with more specific methods (with password vs. force login) --- src/main/java/fr/xephi/authme/api/API.java | 2 +- src/main/java/fr/xephi/authme/api/NewAPI.java | 2 +- .../executable/authme/ForceLoginCommand.java | 2 +- .../executable/login/LoginCommand.java | 2 +- .../fr/xephi/authme/process/Management.java | 8 +- .../authme/process/join/AsynchronousJoin.java | 10 +- .../process/login/AsynchronousLogin.java | 197 ++++++++---- .../process/register/AsyncRegister.java | 5 +- .../fr/xephi/authme/util/CollectionUtils.java | 12 - .../authme/ForceLoginCommandTest.java | 6 +- .../executable/login/LoginCommandTest.java | 2 +- .../process/login/AsynchronousLoginTest.java | 280 ++++++++++++++++++ 12 files changed, 430 insertions(+), 98 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java diff --git a/src/main/java/fr/xephi/authme/api/API.java b/src/main/java/fr/xephi/authme/api/API.java index 8757bcd5d..0eafe3dac 100644 --- a/src/main/java/fr/xephi/authme/api/API.java +++ b/src/main/java/fr/xephi/authme/api/API.java @@ -157,7 +157,7 @@ public class API { * @param player The player to log in */ public static void forceLogin(Player player) { - management.performLogin(player, "dontneed", true); + management.forceLogin(player); } public AuthMe getPlugin() { diff --git a/src/main/java/fr/xephi/authme/api/NewAPI.java b/src/main/java/fr/xephi/authme/api/NewAPI.java index 4ed3fd2e0..68f973a82 100644 --- a/src/main/java/fr/xephi/authme/api/NewAPI.java +++ b/src/main/java/fr/xephi/authme/api/NewAPI.java @@ -179,7 +179,7 @@ public class NewAPI { * @param player The player to log in */ public void forceLogin(Player player) { - management.performLogin(player, "dontneed", true); + management.forceLogin(player); } /** diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java index 706e313e2..9928f56f6 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java @@ -37,7 +37,7 @@ public class ForceLoginCommand implements ExecutableCommand { } else if (!permissionsManager.hasPermission(player, CAN_LOGIN_BE_FORCED)) { sender.sendMessage("You cannot force login the player " + playerName + "!"); } else { - management.performLogin(player, "dontneed", true); + management.forceLogin(player); sender.sendMessage("Force login for " + playerName + " performed!"); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java b/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java index 8b4054ac9..a5d25abf1 100644 --- a/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java @@ -18,6 +18,6 @@ public class LoginCommand extends PlayerCommand { @Override public void runCommand(Player player, List arguments) { final String password = arguments.get(0); - management.performLogin(player, password, false); + management.performLogin(player, password); } } diff --git a/src/main/java/fr/xephi/authme/process/Management.java b/src/main/java/fr/xephi/authme/process/Management.java index e74c2b78e..9efb5e6d2 100644 --- a/src/main/java/fr/xephi/authme/process/Management.java +++ b/src/main/java/fr/xephi/authme/process/Management.java @@ -47,8 +47,12 @@ public class Management { } - public void performLogin(Player player, String password, boolean forceLogin) { - runTask(() -> asynchronousLogin.login(player, password, forceLogin)); + public void performLogin(Player player, String password) { + runTask(() -> asynchronousLogin.login(player, password)); + } + + public void forceLogin(Player player) { + runTask(() -> asynchronousLogin.forceLogin(player)); } public void performLogout(Player player) { diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 2afc93863..dcd7599d3 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -140,7 +140,7 @@ public class AsynchronousJoin implements AsynchronousProcess { playerCache.removePlayer(name); if (auth != null && auth.getIp().equals(ip)) { service.send(player, MessageKey.SESSION_RECONNECTION); - bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.login(player, "dontneed", true)); + bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.forceLogin(player)); return; } else if (service.getProperty(PluginSettings.SESSIONS_EXPIRE_ON_IP_CHANGE)) { service.send(player, MessageKey.SESSION_EXPIRED); @@ -239,12 +239,8 @@ public class AsynchronousJoin implements AsynchronousProcess { && !"localhost".equalsIgnoreCase(ip) && countOnlinePlayersByIp(ip) > service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP)) { - bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(new Runnable() { - @Override - public void run() { - player.kickPlayer(service.retrieveSingleMessage(MessageKey.SAME_IP_ONLINE)); - } - }); + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask( + () -> player.kickPlayer(service.retrieveSingleMessage(MessageKey.SAME_IP_ONLINE))); return false; } return true; diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index e810b2e38..b63c6ccd5 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -1,5 +1,6 @@ package fr.xephi.authme.process.login; +import com.google.common.annotations.VisibleForTesting; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.CaptchaManager; import fr.xephi.authme.cache.TempbanManager; @@ -35,11 +36,12 @@ import java.util.ArrayList; import java.util.List; /** + * Asynchronous task for a player login. */ public class AsynchronousLogin implements AsynchronousProcess { @Inject - private DataSource database; + private DataSource dataSource; @Inject private ProcessService service; @@ -71,23 +73,50 @@ public class AsynchronousLogin implements AsynchronousProcess { @Inject private PlayerDataTaskManager playerDataTaskManager; - AsynchronousLogin() { } + AsynchronousLogin() { + } + + /** + * Processes a player's login request. + * + * @param player the player to log in + * @param password the password to log in with + */ + public void login(Player player, String password) { + PlayerAuth auth = getPlayerAuth(player); + if (auth != null && checkPlayerInfo(player, auth, password)) { + performLogin(player, auth); + } + } + + /** + * Logs a player in without requiring a password. + * + * @param player the player to log in + */ + public void forceLogin(Player player) { + PlayerAuth auth = getPlayerAuth(player); + if (auth != null) { + performLogin(player, auth); + } + } /** * Checks the precondition for authentication (like user known) and returns - * the playerAuth-State + * the player's {@link PlayerAuth} object. * - * @return PlayerAuth + * @return the PlayerAuth object, or {@code null} if the player doesn't exist or may not log in + * (e.g. because he is already logged in) */ - private PlayerAuth preAuth(Player player) { + private PlayerAuth getPlayerAuth(Player player) { final String name = player.getName().toLowerCase(); if (playerCache.isAuthenticated(name)) { service.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); return null; } - PlayerAuth pAuth = database.getAuth(name); - if (pAuth == null) { + PlayerAuth auth = dataSource.getAuth(name); + if (auth == null) { service.send(player, MessageKey.USER_NOT_REGISTERED); // Recreate the message task to immediately send the message again as response // and to make sure we send the right register message (password vs. email registration) @@ -96,19 +125,15 @@ public class AsynchronousLogin implements AsynchronousProcess { } if (!service.getProperty(DatabaseSettings.MYSQL_COL_GROUP).isEmpty() - && pAuth.getGroupId() == service.getProperty(HooksSettings.NON_ACTIVATED_USERS_GROUP)) { + && auth.getGroupId() == service.getProperty(HooksSettings.NON_ACTIVATED_USERS_GROUP)) { service.send(player, MessageKey.ACCOUNT_NOT_ACTIVATED); return null; } final String ip = Utils.getPlayerIp(player); - if (service.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP) > 0 - && !permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) - && !"127.0.0.1".equalsIgnoreCase(ip) && !"localhost".equalsIgnoreCase(ip)) { - if (isLoggedIp(name, ip)) { - service.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); - return null; - } + if (hasReachedMaxLoggedInPlayersForIp(player, ip)) { + service.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); + return null; } boolean isAsync = service.getProperty(PluginSettings.USE_ASYNC_TASKS); @@ -117,52 +142,90 @@ public class AsynchronousLogin implements AsynchronousProcess { if (!event.canLogin()) { return null; } - return pAuth; + return auth; } - public void login(final Player player, String password, boolean forceLogin) { - PlayerAuth pAuth = preAuth(player); - if (pAuth == null) { - return; - } - + /** + * Checks various conditions for regular player login (not used in force login). + * + * @param player the player requesting to log in + * @param auth the PlayerAuth object of the player + * @param password the password supplied by the player + * @return true if the password matches and all other conditions are met (e.g. no captcha required), + * false otherwise + */ + private boolean checkPlayerInfo(Player player, PlayerAuth auth, String password) { final String name = player.getName().toLowerCase(); - // If Captcha is required send a message to the player and deny to login + // If captcha is required send a message to the player and deny to log in if (captchaManager.isCaptchaRequired(name)) { service.send(player, MessageKey.USAGE_CAPTCHA, captchaManager.getCaptchaCodeOrGenerateNew(name)); - return; + return false; } final String ip = Utils.getPlayerIp(player); // Increase the counts here before knowing the result of the login. - // If the login is successful, we clear the captcha count for the player. captchaManager.increaseCount(name); tempbanManager.increaseCount(ip, name); - String email = pAuth.getEmail(); - boolean passwordVerified = forceLogin || passwordSecurity.comparePassword( - password, pAuth.getPassword(), player.getName()); - if (passwordVerified && player.isOnline()) { - PlayerAuth auth = PlayerAuth.builder() - .name(name) - .realName(player.getName()) - .ip(ip) - .email(email) - .password(pAuth.getPassword()) - .build(); - database.updateSession(auth); + if (passwordSecurity.comparePassword(password, auth.getPassword(), player.getName())) { + return true; + } else { + handleWrongPassword(player, ip); + return false; + } + } + /** + * Handles a login with wrong password. + * + * @param player the player who attempted to log in + * @param ip the ip address of the player + */ + private void handleWrongPassword(Player player, String ip) { + ConsoleLogger.fine(player.getName() + " used the wrong password"); + if (tempbanManager.shouldTempban(ip)) { + tempbanManager.tempbanPlayer(player); + } else if (service.getProperty(RestrictionSettings.KICK_ON_WRONG_PASSWORD)) { + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask( + () -> player.kickPlayer(service.retrieveSingleMessage(MessageKey.WRONG_PASSWORD))); + } else { + service.send(player, MessageKey.WRONG_PASSWORD); + + // If the authentication fails check if Captcha is required and send a message to the player + if (captchaManager.isCaptchaRequired(player.getName())) { + service.send(player, MessageKey.USAGE_CAPTCHA, + captchaManager.getCaptchaCodeOrGenerateNew(player.getName())); + } + } + } + + /** + * Sets the player to the logged in state. + * + * @param player the player to log in + * @param auth the associated PlayerAuth object + */ + private void performLogin(Player player, PlayerAuth auth) { + if (player.isOnline()) { + // Update auth to reflect this new login + final String ip = Utils.getPlayerIp(player); + auth.setRealName(player.getName()); + auth.setLastLogin(System.currentTimeMillis()); + auth.setIp(ip); + dataSource.updateSession(auth); + + // Successful login, so reset the captcha & temp ban count + final String name = player.getName(); captchaManager.resetCounts(name); tempbanManager.resetCount(ip, name); player.setNoDamageTicks(0); - if (!forceLogin) - service.send(player, MessageKey.LOGIN_SUCCESS); - + service.send(player, MessageKey.LOGIN_SUCCESS); displayOtherAccounts(auth, player); + final String email = auth.getEmail(); if (service.getProperty(EmailSettings.RECALL_PLAYERS) && (StringUtils.isEmpty(email) || "your@email.com".equalsIgnoreCase(email))) { service.send(player, MessageKey.ADD_EMAIL_MESSAGE); @@ -172,7 +235,7 @@ public class AsynchronousLogin implements AsynchronousProcess { // makes player isLoggedin via API playerCache.addPlayer(auth); - database.setLogged(name); + dataSource.setLogged(name); // As the scheduling executes the Task most likely after the current // task, we schedule it in the end @@ -183,23 +246,8 @@ public class AsynchronousLogin implements AsynchronousProcess { playerData.clearTasks(); } syncProcessManager.processSyncPlayerLogin(player); - } else if (player.isOnline()) { - ConsoleLogger.fine(player.getName() + " used the wrong password"); - if (service.getProperty(RestrictionSettings.KICK_ON_WRONG_PASSWORD)) { - bukkitService.scheduleSyncTaskFromOptionallyAsyncTask( - () -> player.kickPlayer(service.retrieveSingleMessage(MessageKey.WRONG_PASSWORD))); - } else if (tempbanManager.shouldTempban(ip)) { - tempbanManager.tempbanPlayer(player); - } else { - service.send(player, MessageKey.WRONG_PASSWORD); - - // If the authentication fails check if Captcha is required and send a message to the player - if (captchaManager.isCaptchaRequired(name)) { - service.send(player, MessageKey.USAGE_CAPTCHA, captchaManager.getCaptchaCodeOrGenerateNew(name)); - } - } } else { - ConsoleLogger.warning("Player " + name + " wasn't online during login process, aborted... "); + ConsoleLogger.warning("Player '" + player.getName() + "' wasn't online during login process, aborted..."); } } @@ -208,7 +256,7 @@ public class AsynchronousLogin implements AsynchronousProcess { return; } - List auths = database.getAllAuthsByIp(auth.getIp()); + List auths = dataSource.getAllAuthsByIp(auth.getIp()); if (auths.size() <= 1) { return; } @@ -217,13 +265,13 @@ public class AsynchronousLogin implements AsynchronousProcess { for (String currentName : auths) { Player currentPlayer = bukkitService.getPlayerExact(currentName); if (currentPlayer != null && currentPlayer.isOnline()) { - formattedNames.add(ChatColor.GREEN + currentName); + formattedNames.add(ChatColor.GREEN + currentPlayer.getName() + ChatColor.GRAY); } else { formattedNames.add(currentName); } } - String message = ChatColor.GRAY + StringUtils.join(ChatColor.GRAY + ", ", formattedNames) + "."; + String message = ChatColor.GRAY + String.join(", ", formattedNames) + "."; ConsoleLogger.fine("The user " + player.getName() + " has " + auths.size() + " accounts:"); ConsoleLogger.fine(message); @@ -241,12 +289,31 @@ public class AsynchronousLogin implements AsynchronousProcess { } } - private boolean isLoggedIp(String name, String ip) { + /** + * Checks whether the maximum threshold of logged in player per IP address has been reached + * for the given player and IP address. + * + * @param player the player to process + * @param ip the associated ip address + * @return true if the threshold has been reached, false otherwise + */ + @VisibleForTesting + boolean hasReachedMaxLoggedInPlayersForIp(Player player, String ip) { + // Do not perform the check if player has multiple accounts permission or if IP is localhost + if (service.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP) <= 0 + || permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) + || "127.0.0.1".equalsIgnoreCase(ip) + || "localhost".equalsIgnoreCase(ip)) { + return false; + } + + // Count logged in players with same IP address + final String name = player.getName(); int count = 0; - for (Player player : bukkitService.getOnlinePlayers()) { - if (ip.equalsIgnoreCase(Utils.getPlayerIp(player)) - && database.isLogged(player.getName().toLowerCase()) - && !player.getName().equalsIgnoreCase(name)) { + for (Player onlinePlayer : bukkitService.getOnlinePlayers()) { + if (ip.equalsIgnoreCase(Utils.getPlayerIp(onlinePlayer)) + && !onlinePlayer.getName().equals(name) + && dataSource.isLogged(onlinePlayer.getName().toLowerCase())) { ++count; } } diff --git a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java index c7630fc34..8be5b5404 100644 --- a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java @@ -171,10 +171,9 @@ public class AsyncRegister implements AsynchronousProcess { if (!service.getProperty(RegistrationSettings.FORCE_LOGIN_AFTER_REGISTER) && autoLogin) { if (service.getProperty(PluginSettings.USE_ASYNC_TASKS)) { - bukkitService.runTaskAsynchronously(() -> asynchronousLogin.login(player, "dontneed", true)); + bukkitService.runTaskAsynchronously(() -> asynchronousLogin.forceLogin(player)); } else { - bukkitService.scheduleSyncDelayedTask( - () -> asynchronousLogin.login(player, "dontneed", true), SYNC_LOGIN_DELAY); + bukkitService.scheduleSyncDelayedTask(() -> asynchronousLogin.forceLogin(player), SYNC_LOGIN_DELAY); } } syncProcessManager.processSyncPasswordRegister(player); diff --git a/src/main/java/fr/xephi/authme/util/CollectionUtils.java b/src/main/java/fr/xephi/authme/util/CollectionUtils.java index 6ec617a2c..9644c6cdc 100644 --- a/src/main/java/fr/xephi/authme/util/CollectionUtils.java +++ b/src/main/java/fr/xephi/authme/util/CollectionUtils.java @@ -3,7 +3,6 @@ package fr.xephi.authme.util; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Objects; /** * Utils class for collections. @@ -60,15 +59,4 @@ public final class CollectionUtils { public static boolean isEmpty(Collection coll) { return coll == null || coll.isEmpty(); } - - public static List filterCommonStart(List list1, List list2) { - List commonStart = new ArrayList<>(); - int minSize = Math.min(list1.size(), list2.size()); - int i = 0; - while (i < minSize && Objects.equals(list1.get(i), list2.get(i))) { - commonStart.add(list1.get(i)); - ++i; - } - return commonStart; - } } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java index 9f89fe5aa..bc7fdec0f 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java @@ -17,9 +17,7 @@ import java.util.Collections; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.mockito.BDDMockito.given; -import static org.mockito.Matchers.anyString; 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; @@ -107,7 +105,7 @@ public class ForceLoginCommandTest { // then verify(bukkitService).getPlayerExact(playerName); - verify(management).performLogin(eq(player), anyString(), eq(true)); + verify(management).forceLogin(player); } @Test @@ -125,7 +123,7 @@ public class ForceLoginCommandTest { // then verify(bukkitService).getPlayerExact(senderName); - verify(management).performLogin(eq(player), anyString(), eq(true)); + verify(management).forceLogin(player); } private static Player mockPlayer(boolean isOnline, String name) { diff --git a/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java index 21e938bfc..5e02b4b2c 100644 --- a/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java @@ -55,7 +55,7 @@ public class LoginCommandTest { command.executeCommand(sender, Collections.singletonList("password")); // then - verify(management).performLogin(eq(sender), eq("password"), eq(false)); + verify(management).performLogin(eq(sender), eq("password")); } } diff --git a/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java b/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java new file mode 100644 index 000000000..2171bef20 --- /dev/null +++ b/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java @@ -0,0 +1,280 @@ +package fr.xephi.authme.process.login; + +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.AuthMeAsyncPreLoginEvent; +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.process.ProcessService; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.task.PlayerDataTaskManager; +import fr.xephi.authme.util.BukkitService; +import org.bukkit.entity.Player; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import java.util.Arrays; +import java.util.Collection; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.internal.verification.VerificationModeFactory.only; + +/** + * Test for {@link AsynchronousLogin}. + */ +@RunWith(MockitoJUnitRunner.class) +public class AsynchronousLoginTest { + + @InjectMocks + @Spy + private AsynchronousLogin asynchronousLogin; + + @Mock + private DataSource dataSource; + @Mock + private PlayerCache playerCache; + @Mock + private ProcessService processService; + @Mock + private PlayerDataTaskManager playerDataTaskManager; + @Mock + private BukkitService bukkitService; + @Mock + private PermissionsManager permissionsManager; + + @BeforeClass + public static void initLogger() { + TestHelper.setupLogger(); + } + + @Test + public void shouldNotForceLoginAlreadyLoggedInPlayer() { + // given + String name = "bobby"; + Player player = mockPlayer(name); + given(playerCache.isAuthenticated(name)).willReturn(true); + + // when + asynchronousLogin.forceLogin(player); + + // then + verify(playerCache, only()).isAuthenticated(name); + verify(processService).send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldNotForceLoginNonExistentUser() { + // given + String name = "oscar"; + Player player = mockPlayer(name); + given(playerCache.isAuthenticated(name)).willReturn(false); + given(dataSource.getAuth(name)).willReturn(null); + + // when + asynchronousLogin.forceLogin(player); + + // then + verify(playerCache, only()).isAuthenticated(name); + verify(processService).send(player, MessageKey.USER_NOT_REGISTERED); + verify(dataSource, only()).getAuth(name); + } + + @Test + public void shouldNotForceLoginInactiveUser() { + // given + String name = "oscar"; + Player player = mockPlayer(name); + given(playerCache.isAuthenticated(name)).willReturn(false); + int groupId = 13; + PlayerAuth auth = PlayerAuth.builder().name(name).groupId(groupId).build(); + given(dataSource.getAuth(name)).willReturn(auth); + given(processService.getProperty(DatabaseSettings.MYSQL_COL_GROUP)).willReturn("group"); + given(processService.getProperty(HooksSettings.NON_ACTIVATED_USERS_GROUP)).willReturn(groupId); + + // when + asynchronousLogin.forceLogin(player); + + // then + verify(playerCache, only()).isAuthenticated(name); + verify(processService).send(player, MessageKey.ACCOUNT_NOT_ACTIVATED); + verify(dataSource, only()).getAuth(name); + } + + @Test + public void shouldNotForceLoginUserWithAlreadyOnlineIp() { + // given + String name = "oscar"; + String ip = "127.0.12.245"; + Player player = mockPlayer(name); + TestHelper.mockPlayerIp(player, ip); + given(playerCache.isAuthenticated(name)).willReturn(false); + PlayerAuth auth = PlayerAuth.builder().name(name).build(); + given(dataSource.getAuth(name)).willReturn(auth); + given(processService.getProperty(DatabaseSettings.MYSQL_COL_GROUP)).willReturn(""); + doReturn(true).when(asynchronousLogin).hasReachedMaxLoggedInPlayersForIp(any(Player.class), anyString()); + + // when + asynchronousLogin.forceLogin(player); + + // then + verify(playerCache, only()).isAuthenticated(name); + verify(processService).send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); + verify(dataSource, only()).getAuth(name); + verify(asynchronousLogin).hasReachedMaxLoggedInPlayersForIp(player, ip); + } + + @Test + public void shouldNotForceLoginForCanceledEvent() { + // given + String name = "oscar"; + String ip = "127.0.12.245"; + Player player = mockPlayer(name); + TestHelper.mockPlayerIp(player, ip); + given(playerCache.isAuthenticated(name)).willReturn(false); + PlayerAuth auth = PlayerAuth.builder().name(name).build(); + given(dataSource.getAuth(name)).willReturn(auth); + given(processService.getProperty(DatabaseSettings.MYSQL_COL_GROUP)).willReturn(""); + given(processService.getProperty(PluginSettings.USE_ASYNC_TASKS)).willReturn(true); + doReturn(false).when(asynchronousLogin).hasReachedMaxLoggedInPlayersForIp(any(Player.class), anyString()); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + invocation.getArgumentAt(0, AuthMeAsyncPreLoginEvent.class).setCanLogin(false); + return null; + } + }).when(bukkitService).callEvent(any(AuthMeAsyncPreLoginEvent.class)); + + // when + asynchronousLogin.forceLogin(player); + + // then + verify(playerCache, only()).isAuthenticated(name); + verify(dataSource, only()).getAuth(name); + verify(asynchronousLogin).hasReachedMaxLoggedInPlayersForIp(player, ip); + } + + + @Test + public void shouldPassMaxLoginPerIpCheck() { + // given + Player player = mockPlayer("Carl"); + given(processService.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP)).willReturn(2); + given(permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)).willReturn(false); + mockOnlinePlayersInBukkitService(); + + // when + boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "127.0.0.4"); + + // then + assertThat(result, equalTo(false)); + verify(permissionsManager).hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS); + verify(bukkitService).getOnlinePlayers(); + } + + @Test + public void shouldSkipIpCheckForZeroThreshold() { + // given + Player player = mockPlayer("Fiona"); + given(processService.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP)).willReturn(0); + + // when + boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "192.168.0.1"); + + // then + assertThat(result, equalTo(false)); + verifyZeroInteractions(bukkitService); + } + + @Test + public void shouldSkipIpCheckForPlayerWithMultipleAccountsPermission() { + // given + Player player = mockPlayer("Frank"); + given(processService.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP)).willReturn(1); + given(permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)).willReturn(true); + + // when + boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "127.0.0.4"); + + // then + assertThat(result, equalTo(false)); + verify(permissionsManager).hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS); + verifyZeroInteractions(bukkitService); + } + + @Test + public void shouldFailIpCheckForIpWithTooManyPlayersOnline() { + // given + Player player = mockPlayer("Ian"); + given(processService.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP)).willReturn(2); + given(permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)).willReturn(false); + mockOnlinePlayersInBukkitService(); + + // when + boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "192.168.0.1"); + + // then + assertThat(result, equalTo(true)); + verify(permissionsManager).hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS); + verify(bukkitService).getOnlinePlayers(); + } + + private static Player mockPlayer(String name) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + return player; + } + + @SuppressWarnings("unchecked") + private void mockOnlinePlayersInBukkitService() { + // 127.0.0.4: albania (online), brazil (offline) + Player playerA = mockPlayer("albania"); + TestHelper.mockPlayerIp(playerA, "127.0.0.4"); + given(dataSource.isLogged(playerA.getName())).willReturn(true); + Player playerB = mockPlayer("brazil"); + TestHelper.mockPlayerIp(playerB, "127.0.0.4"); + given(dataSource.isLogged(playerB.getName())).willReturn(false); + + // 192.168.0.1: congo (online), denmark (offline), ecuador (online) + Player playerC = mockPlayer("congo"); + TestHelper.mockPlayerIp(playerC, "192.168.0.1"); + given(dataSource.isLogged(playerC.getName())).willReturn(true); + Player playerD = mockPlayer("denmark"); + TestHelper.mockPlayerIp(playerD, "192.168.0.1"); + given(dataSource.isLogged(playerD.getName())).willReturn(false); + Player playerE = mockPlayer("ecuador"); + TestHelper.mockPlayerIp(playerE, "192.168.0.1"); + given(dataSource.isLogged(playerE.getName())).willReturn(true); + + // 192.168.0.0: france (offline) + Player playerF = mockPlayer("france"); + TestHelper.mockPlayerIp(playerF, "192.168.0.0"); + given(dataSource.isLogged(playerF.getName())).willReturn(false); + + Collection onlinePlayers = Arrays.asList(playerA, playerB, playerC, playerD, playerE, playerF); + given(bukkitService.getOnlinePlayers()).willReturn(onlinePlayers); + } + +}