diff --git a/src/main/java/fr/xephi/authme/initialization/TaskCloser.java b/src/main/java/fr/xephi/authme/initialization/TaskCloser.java index d4119f92f..c11269755 100644 --- a/src/main/java/fr/xephi/authme/initialization/TaskCloser.java +++ b/src/main/java/fr/xephi/authme/initialization/TaskCloser.java @@ -1,5 +1,6 @@ package fr.xephi.authme.initialization; +import com.google.common.annotations.VisibleForTesting; import fr.xephi.authme.AuthMe; import fr.xephi.authme.datasource.DataSource; import org.bukkit.scheduler.BukkitScheduler; @@ -50,7 +51,7 @@ public class TaskCloser implements Runnable { } try { - Thread.sleep(1000); + sleep(); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); break; @@ -73,6 +74,12 @@ public class TaskCloser implements Runnable { } } + /** Makes the current thread sleep for one second. */ + @VisibleForTesting + void sleep() throws InterruptedException { + Thread.sleep(1000); + } + private List getPendingTasks() { List pendingTasks = new ArrayList<>(); //returns only the async tasks 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 82fea4db5..5d9a0fca1 100644 --- a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java +++ b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java @@ -96,6 +96,8 @@ public class AsynchronousUnregister implements AsynchronousProcess { ConsoleLogger.info(name + " was unregistered by " + initiator.getName()); service.send(initiator, MessageKey.UNREGISTERED_SUCCESS); } + } else if (initiator != null) { + service.send(initiator, MessageKey.ERROR); } } diff --git a/src/test/java/fr/xephi/authme/initialization/TaskCloserTest.java b/src/test/java/fr/xephi/authme/initialization/TaskCloserTest.java new file mode 100644 index 000000000..d474ac6f5 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/TaskCloserTest.java @@ -0,0 +1,155 @@ +package fr.xephi.authme.initialization; + +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.datasource.DataSource; +import org.bukkit.Server; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginLogger; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitWorker; +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.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link TaskCloser}. + */ +@RunWith(MockitoJUnitRunner.class) +public class TaskCloserTest { + + private static final int[] ACTIVE_WORKERS_ID = {2, 5}; + + private TaskCloser taskCloser; + @Mock + private AuthMe authMe; + @Mock + private PluginLogger logger; + @Mock + private BukkitScheduler bukkitScheduler; + @Mock + private DataSource dataSource; + + @Before + public void initAuthMe() { + Server server = mock(Server.class); + given(server.getScheduler()).willReturn(bukkitScheduler); + ReflectionTestUtils.setField(JavaPlugin.class, authMe, "server", server); + ReflectionTestUtils.setField(JavaPlugin.class, authMe, "logger", logger); + taskCloser = spy(new TaskCloser(authMe, dataSource)); + } + + @Test + public void shouldWaitForTasksToClose() throws InterruptedException { + // given + doNothing().when(taskCloser).sleep(); // avoid sleeping in tests + mockActiveWorkers(); + given(bukkitScheduler.isCurrentlyRunning(ACTIVE_WORKERS_ID[0])).willReturn(false); + given(bukkitScheduler.isCurrentlyRunning(ACTIVE_WORKERS_ID[1])) + .willReturn(true) // first time + .willReturn(false); // second time + + // when + taskCloser.run(); + + // then + verify(bukkitScheduler, times(3)).isQueued(anyInt()); + ArgumentCaptor taskIds = ArgumentCaptor.forClass(Integer.class); + verify(bukkitScheduler, times(3)).isCurrentlyRunning(taskIds.capture()); + assertThat(taskIds.getAllValues(), contains(ACTIVE_WORKERS_ID[0], ACTIVE_WORKERS_ID[1], ACTIVE_WORKERS_ID[1])); + verify(taskCloser, times(2)).sleep(); + verify(dataSource).close(); + } + + @Test + public void shouldAbortForNeverEndingTask() throws InterruptedException { + // given + doNothing().when(taskCloser).sleep(); // avoid sleeping in tests + mockActiveWorkers(); + // This task never ends + given(bukkitScheduler.isCurrentlyRunning(ACTIVE_WORKERS_ID[0])).willReturn(true); + given(bukkitScheduler.isCurrentlyRunning(ACTIVE_WORKERS_ID[1])).willReturn(false); + + // when + taskCloser.run(); + + // then + verify(bukkitScheduler, times(3)).isQueued(anyInt()); + verify(bukkitScheduler, times(61)).isCurrentlyRunning(anyInt()); + verify(taskCloser, times(60)).sleep(); + verify(dataSource).close(); + } + + @Test + public void shouldStopForInterruptedThread() throws InterruptedException, ExecutionException { + // Note ljacqu 20160827: This test must be run in its own thread because we throw an InterruptedException. + // Somehow the java.nio.Files API used in tests that are run subsequently don't like this and fail otherwise. + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.submit(new Runnable() { + @Override + public void run() { + try { + shouldStopForInterruptedThread0(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + }).get(); + } + + /** Test implementation for {@link #shouldStopForInterruptedThread()}. */ + private void shouldStopForInterruptedThread0() throws InterruptedException { + // given + taskCloser = spy(new TaskCloser(authMe, null)); + // First two times do nothing, third time throw exception when we sleep + doNothing().doNothing().doThrow(InterruptedException.class).when(taskCloser).sleep(); + mockActiveWorkers(); + given(bukkitScheduler.isCurrentlyRunning(anyInt())).willReturn(true); + + // when + taskCloser.run(); + + // then + verify(bukkitScheduler, times(4)).isCurrentlyRunning(anyInt()); + verify(taskCloser, times(3)).sleep(); + } + + private void mockActiveWorkers() { + Plugin otherOwner = mock(Plugin.class); + List tasks = Arrays.asList( + mockBukkitWorker(authMe, ACTIVE_WORKERS_ID[0], false), + mockBukkitWorker(otherOwner, 3, false), + mockBukkitWorker(authMe, ACTIVE_WORKERS_ID[1], false), + mockBukkitWorker(authMe, 7, true), + mockBukkitWorker(otherOwner, 11, true)); + given(bukkitScheduler.getActiveWorkers()).willReturn(tasks); + } + + private BukkitWorker mockBukkitWorker(Plugin owner, int taskId, boolean isQueued) { + BukkitWorker worker = mock(BukkitWorker.class); + given(worker.getOwner()).willReturn(owner); + given(worker.getTaskId()).willReturn(taskId); + given(bukkitScheduler.isQueued(taskId)).willReturn(isQueued); + return worker; + } +} diff --git a/src/test/java/fr/xephi/authme/process/unregister/AsynchronousUnregisterTest.java b/src/test/java/fr/xephi/authme/process/unregister/AsynchronousUnregisterTest.java index 02035e329..c77d4d3cf 100644 --- a/src/test/java/fr/xephi/authme/process/unregister/AsynchronousUnregisterTest.java +++ b/src/test/java/fr/xephi/authme/process/unregister/AsynchronousUnregisterTest.java @@ -16,6 +16,7 @@ import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.task.PlayerDataTaskManager; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.TeleportationService; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.BeforeClass; import org.junit.Test; @@ -27,6 +28,7 @@ import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -118,4 +120,173 @@ public class AsynchronousUnregisterTest { verify(bukkitService).runTask(any(Runnable.class)); } + @Test + public void shouldPerformUnregisterAndNotApplyBlindEffect() { + // given + Player player = mock(Player.class); + String name = "Frank21"; + given(player.getName()).willReturn(name); + given(player.isOnline()).willReturn(true); + PlayerAuth auth = mock(PlayerAuth.class); + given(playerCache.getAuth(name)).willReturn(auth); + HashedPassword password = new HashedPassword("password", "in_auth_obj"); + given(auth.getPassword()).willReturn(password); + String userPassword = "pass"; + given(passwordSecurity.comparePassword(userPassword, password, name)).willReturn(true); + given(dataSource.removeAuth(name)).willReturn(true); + given(service.getProperty(RegistrationSettings.FORCE)).willReturn(true); + given(service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)).willReturn(true); + given(service.getProperty(RestrictionSettings.TIMEOUT)).willReturn(0); + + // when + asynchronousUnregister.unregister(player, userPassword); + + // then + verify(service).send(player, MessageKey.UNREGISTERED_SUCCESS); + verify(passwordSecurity).comparePassword(userPassword, password, name); + verify(dataSource).removeAuth(name); + verify(playerCache).removePlayer(name); + verify(teleportationService).teleportOnJoin(player); + verify(authGroupHandler).setGroup(player, AuthGroupType.UNREGISTERED); + verify(bukkitService).runTask(any(Runnable.class)); + } + + @Test + public void shouldNotApplyUnregisteredEffectsForNotForcedRegistration() { + // given + Player player = mock(Player.class); + String name = "__FranK"; + given(player.getName()).willReturn(name); + given(player.isOnline()).willReturn(true); + String userPassword = "141$$5ad"; + HashedPassword password = new HashedPassword("ttt123"); + PlayerAuth auth = PlayerAuth.builder().name(name).password(password).build(); + given(playerCache.getAuth(name)).willReturn(auth); + given(passwordSecurity.comparePassword(userPassword, password, name)).willReturn(true); + given(dataSource.removeAuth(name)).willReturn(true); + given(service.getProperty(RegistrationSettings.FORCE)).willReturn(false); + + // when + asynchronousUnregister.unregister(player, userPassword); + + // then + verify(service).send(player, MessageKey.UNREGISTERED_SUCCESS); + verify(passwordSecurity).comparePassword(userPassword, password, name); + verify(dataSource).removeAuth(name); + verify(playerCache).removePlayer(name); + verify(authGroupHandler).setGroup(player, AuthGroupType.UNREGISTERED); + verifyZeroInteractions(teleportationService, playerDataTaskManager); + verify(bukkitService, never()).runTask(any(Runnable.class)); + } + + @Test + public void shouldHandleDatabaseError() { + // given + Player player = mock(Player.class); + String name = "Frank21"; + given(player.getName()).willReturn(name); + given(player.isOnline()).willReturn(true); + PlayerAuth auth = mock(PlayerAuth.class); + given(playerCache.getAuth(name)).willReturn(auth); + HashedPassword password = new HashedPassword("password", "in_auth_obj"); + given(auth.getPassword()).willReturn(password); + String userPassword = "pass"; + given(passwordSecurity.comparePassword(userPassword, password, name)).willReturn(true); + given(dataSource.removeAuth(name)).willReturn(false); + + // when + asynchronousUnregister.unregister(player, userPassword); + + // then + verify(passwordSecurity).comparePassword(userPassword, password, name); + verify(dataSource).removeAuth(name); + verify(service).send(player, MessageKey.ERROR); + verifyZeroInteractions(teleportationService, authGroupHandler, bukkitService); + } + + @Test + public void shouldNotTeleportOfflinePlayer() { + // given + Player player = mock(Player.class); + String name = "Frank21"; + given(player.getName()).willReturn(name); + given(player.isOnline()).willReturn(false); + PlayerAuth auth = mock(PlayerAuth.class); + given(playerCache.getAuth(name)).willReturn(auth); + HashedPassword password = new HashedPassword("password", "in_auth_obj"); + given(auth.getPassword()).willReturn(password); + String userPassword = "pass"; + given(passwordSecurity.comparePassword(userPassword, password, name)).willReturn(true); + given(dataSource.removeAuth(name)).willReturn(true); + given(service.getProperty(RegistrationSettings.FORCE)).willReturn(true); + given(service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)).willReturn(true); + given(service.getProperty(RestrictionSettings.TIMEOUT)).willReturn(12); + + // when + asynchronousUnregister.unregister(player, userPassword); + + // then + verify(passwordSecurity).comparePassword(userPassword, password, name); + verify(dataSource).removeAuth(name); + verify(playerCache).removePlayer(name); + verifyZeroInteractions(teleportationService, authGroupHandler); + } + + // Initiator known and Player object available + @Test + public void shouldPerformAdminUnregister() { + // given + Player player = mock(Player.class); + String name = "Frank21"; + given(player.getName()).willReturn(name); + given(player.isOnline()).willReturn(true); + given(dataSource.removeAuth(name)).willReturn(true); + given(service.getProperty(RegistrationSettings.FORCE)).willReturn(true); + given(service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)).willReturn(true); + given(service.getProperty(RestrictionSettings.TIMEOUT)).willReturn(12); + CommandSender initiator = mock(CommandSender.class); + + // when + asynchronousUnregister.adminUnregister(initiator, name, player); + + // then + verify(service).send(player, MessageKey.UNREGISTERED_SUCCESS); + verify(service).send(initiator, MessageKey.UNREGISTERED_SUCCESS); + verify(dataSource).removeAuth(name); + verify(playerCache).removePlayer(name); + verify(teleportationService).teleportOnJoin(player); + verify(authGroupHandler).setGroup(player, AuthGroupType.UNREGISTERED); + verify(bukkitService).runTask(any(Runnable.class)); + } + + @Test + public void shouldPerformAdminUnregisterWithoutInitiatorOrPlayer() { + // given + String name = "billy"; + given(dataSource.removeAuth(name)).willReturn(true); + + // when + asynchronousUnregister.adminUnregister(null, name, null); + + // then + verify(dataSource).removeAuth(name); + verify(playerCache).removePlayer(name); + verifyZeroInteractions(authGroupHandler, teleportationService); + } + + @Test + public void shouldHandleDatabaseErrorForAdminUnregister() { + // given + String name = "TtOoLl"; + CommandSender initiator = mock(CommandSender.class); + given(dataSource.removeAuth(name)).willReturn(false); + + // when + asynchronousUnregister.adminUnregister(initiator, name, null); + + // then + verify(dataSource).removeAuth(name); + verify(service).send(initiator, MessageKey.ERROR); + verifyZeroInteractions(playerCache, teleportationService, authGroupHandler); + } }