diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java index 58ccdf0f..5c8196dd 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java @@ -7,31 +7,29 @@ import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.task.MessageTask; import fr.xephi.authme.task.TimeoutTask; +import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitTask; import java.util.List; +import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; + /** * Admin command to unregister a player. */ public class UnregisterAdminCommand implements ExecutableCommand { - @Override public void executeCommand(final CommandSender sender, List arguments, CommandService commandService) { - // AuthMe plugin instance - final AuthMe plugin = AuthMe.getInstance(); - // Get the player name String playerName = arguments.get(0); String playerNameLowerCase = playerName.toLowerCase(); @@ -53,27 +51,44 @@ public class UnregisterAdminCommand implements ExecutableCommand { PlayerCache.getInstance().removePlayer(playerNameLowerCase); Utils.setGroup(target, Utils.GroupType.UNREGISTERED); if (target != null && target.isOnline()) { - Utils.teleportToSpawn(target); - LimboCache.getInstance().addLimboPlayer(target); - int timeOut = Settings.getRegistrationTimeout * 20; - int interval = Settings.getWarnMessageInterval; - BukkitScheduler scheduler = sender.getServer().getScheduler(); - if (timeOut != 0) { - BukkitTask id = scheduler.runTaskLater(plugin, new TimeoutTask(plugin, playerNameLowerCase, target), timeOut); - LimboCache.getInstance().getLimboPlayer(playerNameLowerCase).setTimeoutTask(id); - } - LimboCache.getInstance().getLimboPlayer(playerNameLowerCase).setMessageTask( - scheduler.runTask(plugin, new MessageTask(commandService.getBukkitService(), plugin.getMessages(), - playerNameLowerCase, MessageKey.REGISTER_MESSAGE, interval))); - - if (commandService.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { - target.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeOut, 2)); + if (commandService.getProperty(RegistrationSettings.FORCE)) { + applyUnregisteredEffectsAndTasks(target, commandService); } commandService.send(target, MessageKey.UNREGISTERED_SUCCESS); } // Show a status message commandService.send(sender, MessageKey.UNREGISTERED_SUCCESS); - ConsoleLogger.info(playerName + " unregistered"); + ConsoleLogger.info(sender.getName() + " unregistered " + playerName); + } + + /** + * When registration is forced, applies the configured "unregistered effects" to the player as he + * would encounter when joining the server before logging on - reminder task to log in, + * timeout kick, blindness. + * + * @param target the player that was unregistered + * @param service the command service + */ + private void applyUnregisteredEffectsAndTasks(Player target, CommandService service) { + final AuthMe plugin = service.getAuthMe(); + final BukkitService bukkitService = service.getBukkitService(); + final String playerNameLowerCase = target.getName().toLowerCase(); + + Utils.teleportToSpawn(target); + LimboCache.getInstance().addLimboPlayer(target); + int timeOut = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; + int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); + if (timeOut != 0) { + BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(plugin, playerNameLowerCase, target), timeOut); + LimboCache.getInstance().getLimboPlayer(playerNameLowerCase).setTimeoutTask(id); + } + LimboCache.getInstance().getLimboPlayer(playerNameLowerCase).setMessageTask( + bukkitService.runTask(new MessageTask(service.getBukkitService(), plugin.getMessages(), + playerNameLowerCase, MessageKey.REGISTER_MESSAGE, interval))); + + if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { + target.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeOut, 2)); + } } } diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java index 84288822..b9505143 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -63,7 +63,12 @@ public class ProcessSyncPasswordRegister implements Process { } } - private void forceLogin(Player player) { + /** + * Request that the player log in. + * + * @param player the player + */ + private void requestLogin(Player player) { Utils.teleportToSpawn(player); LimboCache cache = LimboCache.getInstance(); cache.updateLimboPlayer(player); @@ -131,22 +136,12 @@ public class ProcessSyncPasswordRegister implements Process { return; } - // Register is finish and player is logged, display welcome message - if (service.getProperty(RegistrationSettings.USE_WELCOME_MESSAGE)) { - if (service.getProperty(RegistrationSettings.BROADCAST_WELCOME_MESSAGE)) { - for (String s : service.getSettings().getWelcomeMessage()) { - plugin.getServer().broadcastMessage(plugin.replaceAllInfo(s, player)); - } - } else { - for (String s : service.getSettings().getWelcomeMessage()) { - player.sendMessage(plugin.replaceAllInfo(s, player)); - } - } - } + // Register is now finished; we can force all commands + forceCommands(); - // Request Login after Registration + // Request login after registration if (service.getProperty(RegistrationSettings.FORCE_LOGIN_AFTER_REGISTER)) { - forceLogin(player); + requestLogin(player); return; } @@ -154,9 +149,6 @@ public class ProcessSyncPasswordRegister implements Process { sendBungeeMessage(); } - // Register is now finished; we can force all commands - forceCommands(); - sendTo(); } diff --git a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java index 7b2c2a7f..cd3f41a2 100644 --- a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java +++ b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java @@ -97,7 +97,6 @@ public class AsynchronousUnregister implements Process { } service.send(player, MessageKey.UNREGISTERED_SUCCESS); ConsoleLogger.info(player.getDisplayName() + " unregistered himself"); - Utils.teleportToSpawn(player); } else { service.send(player, MessageKey.WRONG_PASSWORD); } diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index 129f35b3..944f675d 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -39,8 +39,7 @@ public final class Settings { public static String getNickRegex, getUnloggedinGroup, unRegisteredGroup, backupWindowsPath, getRegisteredGroup, rakamakUsers, rakamakUsersIp, defaultWorld, crazyloginFileName; - public static int getWarnMessageInterval, getSessionTimeout, - getRegistrationTimeout, getMaxNickLength, getMinNickLength, + public static int getSessionTimeout, getMaxNickLength, getMinNickLength, getNonActivatedGroup, maxLoginTry, captchaLength, getMaxLoginPerIp; protected static FileConfiguration configFile; @@ -58,10 +57,8 @@ public final class Settings { isPermissionCheckEnabled = load(PluginSettings.ENABLE_PERMISSION_CHECK); isForcedRegistrationEnabled = load(RegistrationSettings.FORCE); isTeleportToSpawnEnabled = load(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN); - getWarnMessageInterval = load(RegistrationSettings.MESSAGE_INTERVAL); isSessionsEnabled = load(PluginSettings.SESSIONS_ENABLED); getSessionTimeout = configFile.getInt("settings.sessions.timeout", 10); - getRegistrationTimeout = load(RestrictionSettings.TIMEOUT); getMaxNickLength = configFile.getInt("settings.restrictions.maxNicknameLength", 20); getMinNickLength = configFile.getInt("settings.restrictions.minNicknameLength", 3); getNickRegex = configFile.getString("settings.restrictions.allowedNicknameCharacters", "[a-zA-Z0-9_?]*"); diff --git a/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java b/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java index c2c2bbe3..db1cb1ee 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java @@ -67,7 +67,7 @@ public class RegistrationSettings implements SettingsClass { newListProperty("settings.forceRegisterCommandsAsConsole"); @Comment({ - "Enable to display the welcome message (welcome.txt) after a registration or a login", + "Enable to display the welcome message (welcome.txt) after a login", "You can use colors in this welcome.txt + some replaced strings:", "{PLAYER}: player name, {ONLINE}: display number of online players, {MAXPLAYERS}: display server slots,", "{IP}: player ip, {LOGINS}: number of players logged, {WORLD}: player current world, {SERVER}: server name", diff --git a/src/main/java/fr/xephi/authme/util/MigrationService.java b/src/main/java/fr/xephi/authme/util/MigrationService.java index 86f98e4c..7bf028f6 100644 --- a/src/main/java/fr/xephi/authme/util/MigrationService.java +++ b/src/main/java/fr/xephi/authme/util/MigrationService.java @@ -37,12 +37,17 @@ public final class MigrationService { if (HashAlgorithm.PLAINTEXT == settings.getProperty(SecuritySettings.PASSWORD_HASH)) { ConsoleLogger.showError("Your HashAlgorithm has been detected as plaintext and is now deprecated;" + " it will be changed and hashed now to the AuthMe default hashing method"); + ConsoleLogger.showError("Don't stop your server; wait for the conversion to have been completed!"); List allAuths = dataSource.getAllAuths(); for (PlayerAuth auth : allAuths) { - HashedPassword hashedPassword = authmeSha256.computeHash( - auth.getPassword().getHash(), auth.getNickname()); - auth.setPassword(hashedPassword); - dataSource.updatePassword(auth); + String hash = auth.getPassword().getHash(); + if (hash.startsWith("$SHA$")) { + ConsoleLogger.showError("Skipping conversion for " + auth.getNickname() + "; detected SHA hash"); + } else { + HashedPassword hashedPassword = authmeSha256.computeHash(hash, auth.getNickname()); + auth.setPassword(hashedPassword); + dataSource.updatePassword(auth); + } } settings.setProperty(SecuritySettings.PASSWORD_HASH, HashAlgorithm.SHA256); settings.save(); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 86aecfaa..17b62b37 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -253,11 +253,11 @@ settings: forceRegisterCommands: [] # Force these commands after /register as a server console, without any '/', use %p for replace with player name forceRegisterCommandsAsConsole: [] - # Do we need to display the welcome message (welcome.txt) after a register or a login? - # You can use colors in this welcome.txt + some replaced strings : - # {PLAYER} : player name, {ONLINE} : display number of online players, {MAXPLAYERS} : display server slots, - # {IP} : player ip, {LOGINS} : number of players logged, {WORLD} : player current world, {SERVER} : server name - # {VERSION} : get current bukkit version, {COUNTRY} : player country + # Do we need to display the welcome message (welcome.txt) after a login? + # You can use colors in this welcome.txt + some replaced strings: + # {PLAYER}: player name, {ONLINE}: display number of online players, {MAXPLAYERS}: display server slots, + # {IP}: player ip, {LOGINS}: number of players logged, {WORLD}: player current world, {SERVER}: server name + # {VERSION}: get current bukkit version, {COUNTRY}: player country useWelcomeMessage: true # Do we need to broadcast the welcome message to all server or only to the player? set true for server or false for player broadcastWelcomeMessage: false diff --git a/src/test/java/fr/xephi/authme/TestHelper.java b/src/test/java/fr/xephi/authme/TestHelper.java index fe55de5f..463b3870 100644 --- a/src/test/java/fr/xephi/authme/TestHelper.java +++ b/src/test/java/fr/xephi/authme/TestHelper.java @@ -6,6 +6,9 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; @@ -84,9 +87,42 @@ public final class TestHelper { runnable.run(); } + /** + * Assign the necessary fields on ConsoleLogger with mocks. + * + * @return The logger mock used + */ public static Logger setupLogger() { Logger logger = Mockito.mock(Logger.class); ConsoleLogger.setLogger(logger); return logger; } + + /** + * Check that a class only has a hidden, zero-argument constructor, preventing the + * instantiation of such classes (utility classes). Invokes the hidden constructor + * as to register the code coverage. + * + * @param clazz The class to validate + */ + public static void validateHasOnlyPrivateEmptyConstructor(Class clazz) { + Constructor[] constructors = clazz.getDeclaredConstructors(); + if (constructors.length > 1) { + throw new IllegalStateException("Class " + clazz.getSimpleName() + " has more than one constructor"); + } else if (constructors[0].getParameterTypes().length != 0) { + throw new IllegalStateException("Constructor of " + clazz + " does not have empty parameter list"); + } else if (!Modifier.isPrivate(constructors[0].getModifiers())) { + throw new IllegalStateException("Constructor of " + clazz + " is not private"); + } + + // Ugly hack to get coverage on the private constructors + // http://stackoverflow.com/questions/14077842/how-to-test-a-private-constructor-in-java-application + try { + constructors[0].setAccessible(true); + constructors[0].newInstance(); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new UnsupportedOperationException(e); + } + } + } diff --git a/src/test/java/fr/xephi/authme/events/EventsConsistencyTest.java b/src/test/java/fr/xephi/authme/events/EventsConsistencyTest.java new file mode 100644 index 00000000..d7ba5f74 --- /dev/null +++ b/src/test/java/fr/xephi/authme/events/EventsConsistencyTest.java @@ -0,0 +1,94 @@ +package fr.xephi.authme.events; + +import org.apache.commons.lang.reflect.MethodUtils; +import org.bukkit.event.Event; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Checks the consistency of the AuthMe event classes. + */ +public class EventsConsistencyTest { + + private static final String SRC_FOLDER = "src/main/java/"; + private static final String EVENTS_FOLDER = SRC_FOLDER + "/fr/xephi/authme/events/"; + private static List> classes; + + @BeforeClass + public static void scanEventClasses() { + File eventsFolder = new File(EVENTS_FOLDER); + File[] filesInFolder = eventsFolder.listFiles(); + if (filesInFolder == null || filesInFolder.length == 0) { + throw new IllegalStateException("Could not read folder '" + EVENTS_FOLDER + "'. Is it correct?"); + } + + classes = new ArrayList<>(); + for (File file : filesInFolder) { + Class clazz = getEventClassFromFile(file); + if (clazz != null) { + classes.add(clazz); + } + } + if (classes.isEmpty()) { + throw new IllegalStateException("Did not find any AuthMe event classes. Is the folder correct?"); + } + } + + @Test + public void shouldExtendFromCustomEvent() { + for (Class clazz : classes) { + assertThat("Class " + clazz.getSimpleName() + " is subtype of CustomEvent", + CustomEvent.class.isAssignableFrom(clazz), equalTo(true)); + } + } + + /** + * Bukkit requires a static getHandlerList() method on all event classes, see {@link Event}. + * This test checks that such a method is present, and that it is absent if the class + * is not instantiable (abstract class). + */ + @Test + public void shouldHaveStaticEventHandlerMethod() { + for (Class clazz : classes) { + Method handlerListMethod = MethodUtils.getAccessibleMethod(clazz, "getHandlerList", new Class[]{}); + if (canBeInstantiated(clazz)) { + assertThat("Class " + clazz.getSimpleName() + " has static method getHandlerList()", + handlerListMethod != null && Modifier.isStatic(handlerListMethod.getModifiers()), equalTo(true)); + } else { + assertThat("Non-instantiable class " + clazz.getSimpleName() + " does not have static getHandlerList()", + handlerListMethod, nullValue()); + } + } + } + + private static boolean canBeInstantiated(Class clazz) { + return !clazz.isInterface() && !clazz.isEnum() && !Modifier.isAbstract(clazz.getModifiers()); + } + + private static Class getEventClassFromFile(File file) { + String fileName = file.getPath(); + String className = fileName + .substring(SRC_FOLDER.length(), fileName.length() - ".java".length()) + .replace(File.separator, "."); + try { + Class clazz = EventsConsistencyTest.class.getClassLoader().loadClass(className); + if (Event.class.isAssignableFrom(clazz)) { + return (Class) clazz; + } + return null; + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Could not load class '" + className + "'", e); + } + } + +} diff --git a/src/test/java/fr/xephi/authme/settings/properties/SettingsClassConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/properties/SettingsClassConsistencyTest.java index c6b00383..75c1a56a 100644 --- a/src/test/java/fr/xephi/authme/settings/properties/SettingsClassConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/settings/properties/SettingsClassConsistencyTest.java @@ -1,22 +1,20 @@ package fr.xephi.authme.settings.properties; import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.settings.domain.SettingsClass; import org.junit.BeforeClass; import org.junit.Test; import java.io.File; -import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; @@ -86,24 +84,9 @@ public class SettingsClassConsistencyTest { } @Test - public void shouldHaveHiddenDefaultConstructorOnly() { + public void shouldHaveHiddenEmptyConstructorOnly() { for (Class clazz : classes) { - Constructor[] constructors = clazz.getDeclaredConstructors(); - assertThat(clazz + " should only have one constructor", - constructors, arrayWithSize(1)); - assertThat("Constructor of " + clazz + " is private", - Modifier.isPrivate(constructors[0].getModifiers()), equalTo(true)); - - // Ugly hack to get coverage on the private constructors - // http://stackoverflow.com/questions/14077842/how-to-test-a-private-constructor-in-java-application - try { - Constructor constructor = clazz.getDeclaredConstructor(); - constructor.setAccessible(true); - constructor.newInstance(); - } catch (NoSuchMethodException | InstantiationException - | IllegalAccessException | InvocationTargetException e) { - e.printStackTrace(); - } + TestHelper.validateHasOnlyPrivateEmptyConstructor(clazz); } } diff --git a/src/test/java/fr/xephi/authme/util/MigrationServiceTest.java b/src/test/java/fr/xephi/authme/util/MigrationServiceTest.java new file mode 100644 index 00000000..34fda2bb --- /dev/null +++ b/src/test/java/fr/xephi/authme/util/MigrationServiceTest.java @@ -0,0 +1,134 @@ +package fr.xephi.authme.util; + +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.security.crypts.SHA256; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.SecuritySettings; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import java.util.Arrays; + +import static fr.xephi.authme.AuthMeMatchers.equalToHash; +import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.junit.Assert.assertThat; +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.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Test for {@link MigrationService}. + */ +@RunWith(MockitoJUnitRunner.class) +public class MigrationServiceTest { + + @Mock + private NewSetting settings; + + @Mock + private DataSource dataSource; + + @Mock + private SHA256 sha256; + + @BeforeClass + public static void setUpLogger() { + TestHelper.setupLogger(); + } + + @Test + public void shouldMigratePlaintextHashes() { + // given + PlayerAuth auth1 = authWithNickAndHash("bobby", "test"); + PlayerAuth auth2 = authWithNickAndHash("user", "myPassword"); + PlayerAuth auth3 = authWithNickAndHash("Tester12", "$tester12_pw"); + given(dataSource.getAllAuths()).willReturn(Arrays.asList(auth1, auth2, auth3)); + setSha256MockToUppercase(sha256); + given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.PLAINTEXT); + + // when + MigrationService.changePlainTextToSha256(settings, dataSource, sha256); + + // then + verify(sha256, times(3)).computeHash(anyString(), anyString()); + verify(dataSource).getAllAuths(); // need to verify this because we use verifyNoMoreInteractions() after + verify(dataSource).updatePassword(auth1); + assertThat(auth1.getPassword(), equalToHash("TEST")); + verify(dataSource).updatePassword(auth2); + assertThat(auth2.getPassword(), equalToHash("MYPASSWORD")); + verify(dataSource).updatePassword(auth3); + assertThat(auth3.getPassword(), equalToHash("$TESTER12_PW")); + verifyNoMoreInteractions(dataSource); + verify(settings).setProperty(SecuritySettings.PASSWORD_HASH, HashAlgorithm.SHA256); + } + + @Test + public void shouldNotMigrateShaHashes() { + // given + PlayerAuth auth1 = authWithNickAndHash("testUser", "abc1234"); + PlayerAuth auth2 = authWithNickAndHash("minecraft", "$SHA$f28930ae09823eba4cd98a3"); + given(dataSource.getAllAuths()).willReturn(Arrays.asList(auth1, auth2)); + setSha256MockToUppercase(sha256); + given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.PLAINTEXT); + + // when + MigrationService.changePlainTextToSha256(settings, dataSource, sha256); + + // then + verify(sha256).computeHash(eq("abc1234"), argThat(equalToIgnoringCase("testUser"))); + verifyNoMoreInteractions(sha256); + verify(dataSource).getAllAuths(); // need to verify this because we use verifyNoMoreInteractions() after + verify(dataSource).updatePassword(auth1); + assertThat(auth1.getPassword(), equalToHash("ABC1234")); + verifyNoMoreInteractions(dataSource); + verify(settings).setProperty(SecuritySettings.PASSWORD_HASH, HashAlgorithm.SHA256); + } + + @Test + public void shouldNotMigrateForHashOtherThanPlaintext() { + // given + given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.BCRYPT); + + // when + MigrationService.changePlainTextToSha256(settings, dataSource, sha256); + + // then + verify(settings).getProperty(SecuritySettings.PASSWORD_HASH); + verifyNoMoreInteractions(settings, dataSource, sha256); + } + + @Test + public void shouldHaveHiddenEmptyConstructorOnly() { + TestHelper.validateHasOnlyPrivateEmptyConstructor(MigrationService.class); + } + + private static PlayerAuth authWithNickAndHash(String nick, String hash) { + return PlayerAuth.builder() + .name(nick) + .password(hash, null) + .build(); + } + + private static void setSha256MockToUppercase(SHA256 sha256) { + given(sha256.computeHash(anyString(), anyString())).willAnswer(new Answer() { + @Override + public HashedPassword answer(InvocationOnMock invocation) { + String plainPassword = (String) invocation.getArguments()[0]; + return new HashedPassword(plainPassword.toUpperCase(), null); + } + }); + } +}