diff --git a/src/test/java/world/bentobox/parkour/AbstractParkourTest.java b/src/test/java/world/bentobox/parkour/AbstractParkourTest.java index d929ac3..9e0d134 100644 --- a/src/test/java/world/bentobox/parkour/AbstractParkourTest.java +++ b/src/test/java/world/bentobox/parkour/AbstractParkourTest.java @@ -49,6 +49,7 @@ import world.bentobox.bentobox.managers.PlaceholdersManager; import world.bentobox.bentobox.managers.RanksManager; import world.bentobox.bentobox.util.Util; +import world.bentobox.parkour.mocks.ServerMocks; /** * @author tastybento @@ -96,6 +97,7 @@ public static void beforeClass() throws IllegalAccessException, InvocationTarget @After public void tearDown() throws IOException { + ServerMocks.unsetBukkitServer(); User.clearUsers(); Mockito.framework().clearInlineMocks(); deleteAll(new File("database")); @@ -117,6 +119,7 @@ protected void deleteAll(File file) throws IOException { */ @Before public void setUp() throws Exception { + Server server = ServerMocks.newServer(); // Set up plugin Whitebox.setInternalState(BentoBox.class, "instance", plugin); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger()); @@ -158,7 +161,6 @@ public void setUp() throws Exception { // Server PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); - Server server = mock(Server.class); when(Bukkit.getServer()).thenReturn(server); when(Bukkit.getLogger()).thenReturn(Logger.getAnonymousLogger()); when(Bukkit.getPluginManager()).thenReturn(mock(PluginManager.class)); diff --git a/src/test/java/world/bentobox/parkour/ParkourManagerTest.java b/src/test/java/world/bentobox/parkour/ParkourManagerTest.java index 171b5c6..56b3c8e 100644 --- a/src/test/java/world/bentobox/parkour/ParkourManagerTest.java +++ b/src/test/java/world/bentobox/parkour/ParkourManagerTest.java @@ -47,6 +47,7 @@ import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.managers.PlayersManager; import world.bentobox.bentobox.util.Util; +import world.bentobox.parkour.mocks.ServerMocks; import world.bentobox.parkour.objects.ParkourData; /** @@ -99,6 +100,8 @@ public static void beforeClass() throws IllegalAccessException, InvocationTarget @Before public void setUp() { + ServerMocks.newServer(); + when(addon.getPlugin()).thenReturn(plugin); // Set up plugin Whitebox.setInternalState(BentoBox.class, "instance", plugin); @@ -150,6 +153,7 @@ public void setUp() { */ @After public void tearDown() throws Exception { + ServerMocks.unsetBukkitServer(); deleteAll(new File("database")); User.clearUsers(); Mockito.framework().clearInlineMocks(); diff --git a/src/test/java/world/bentobox/parkour/SettingsTest.java b/src/test/java/world/bentobox/parkour/SettingsTest.java index 402dc03..5b95165 100644 --- a/src/test/java/world/bentobox/parkour/SettingsTest.java +++ b/src/test/java/world/bentobox/parkour/SettingsTest.java @@ -3,26 +3,36 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; import java.util.Collections; import java.util.List; import java.util.Map; +import org.bukkit.Bukkit; import org.bukkit.Difficulty; import org.bukkit.GameMode; +import org.bukkit.Server; import org.bukkit.block.Biome; import org.bukkit.entity.EntityType; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.parkour.mocks.ServerMocks; + /** * @author tastybento * */ @RunWith(PowerMockRunner.class) +@PrepareForTest(Bukkit.class) public class SettingsTest { private Settings s; @@ -32,14 +42,17 @@ public class SettingsTest { */ @Before public void setUp() throws Exception { + Server server = ServerMocks.newServer(); + PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); + when(Bukkit.getServer()).thenReturn(server); s = new Settings(); } - /** - * @throws java.lang.Exception - */ @After - public void tearDown() throws Exception { + public void tearDown() { + ServerMocks.unsetBukkitServer(); + User.clearUsers(); + Mockito.framework().clearInlineMocks(); } /** diff --git a/src/test/java/world/bentobox/parkour/commands/ClearTopCommandTest.java b/src/test/java/world/bentobox/parkour/commands/ClearTopCommandTest.java index d01d036..0a23d2e 100644 --- a/src/test/java/world/bentobox/parkour/commands/ClearTopCommandTest.java +++ b/src/test/java/world/bentobox/parkour/commands/ClearTopCommandTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -24,6 +25,7 @@ import org.bukkit.entity.Player; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,6 +52,7 @@ import world.bentobox.parkour.ParkourManager; import world.bentobox.parkour.Settings; import world.bentobox.parkour.gui.RankingsUI; +import world.bentobox.parkour.mocks.ServerMocks; /** * @author tastybento @@ -96,6 +99,7 @@ public class ClearTopCommandTest { */ @Before public void setUp() throws Exception { + ServerMocks.newServer(); // Set up plugin Whitebox.setInternalState(BentoBox.class, "instance", plugin); world.bentobox.bentobox.Settings s = new world.bentobox.bentobox.Settings(); @@ -166,18 +170,22 @@ public void setUp() throws Exception { // DUT cmd = new ClearTopCommand(ac); } + + @After + public void tearDown() { + ServerMocks.unsetBukkitServer(); + User.clearUsers(); + Mockito.framework().clearInlineMocks(); + } + /** * Test method for {@link world.bentobox.parkour.commands.ClearTopCommand#ClearTopCommand(world.bentobox.bentobox.api.commands.CompositeCommand)}. */ @Test public void testClearTopCommand() { - assertNonNull(cmd); + assertNotNull(cmd); } - private void assertNonNull(ClearTopCommand cmd2) { - // TODO Auto-generated method stub - - } /** * Test method for {@link world.bentobox.parkour.commands.ClearTopCommand#setup()}. */ diff --git a/src/test/java/world/bentobox/parkour/listeners/CourseRunnerListenerTest.java b/src/test/java/world/bentobox/parkour/listeners/CourseRunnerListenerTest.java index be2f56b..8c05df3 100644 --- a/src/test/java/world/bentobox/parkour/listeners/CourseRunnerListenerTest.java +++ b/src/test/java/world/bentobox/parkour/listeners/CourseRunnerListenerTest.java @@ -1,5 +1,6 @@ package world.bentobox.parkour.listeners; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -9,6 +10,7 @@ import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.RETURNS_MOCKS; +import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -32,6 +34,7 @@ import org.bukkit.block.BlockFace; import org.bukkit.entity.Creeper; import org.bukkit.entity.Player; +import org.bukkit.entity.Player.Spigot; import org.bukkit.event.block.Action; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityDamageEvent.DamageCause; @@ -46,11 +49,13 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.stubbing.Answer; import org.powermock.api.mockito.PowerMockito; import org.powermock.modules.junit4.PowerMockRunner; +import net.md_5.bungee.api.chat.TextComponent; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.events.island.IslandEnterEvent; import world.bentobox.bentobox.api.events.island.IslandExitEvent; @@ -100,6 +105,8 @@ public class CourseRunnerListenerTest extends AbstractParkourTest { private Block block; @Mock private User u; + @Mock + private Spigot spigot; /** * @throws java.lang.Exception @@ -118,6 +125,7 @@ public void setUp() throws Exception { when(player.hasPermission(anyString())).thenReturn(false); when(player.getGameMode()).thenReturn(GameMode.SURVIVAL); when(player.getServer()).thenReturn(server); + when(player.spigot()).thenReturn(spigot); User.setPlugin(plugin); user = User.getInstance(player); @@ -414,7 +422,7 @@ public void testOnVisitorCommandQuittingParkour() { public void testOnStartEndSet() { PlayerInteractEvent e = new PlayerInteractEvent(player, Action.PHYSICAL, null, block, BlockFace.DOWN); crl.onStartEndSet(e); - verify(player).sendMessage("parkour.start"); + checkSpigotMessage("parkour.start"); } /** @@ -425,7 +433,7 @@ public void testOnStartEndSetNoEnd() { when(this.parkourManager.getEnd(island)).thenReturn(Optional.empty()); PlayerInteractEvent e = new PlayerInteractEvent(player, Action.PHYSICAL, null, block, BlockFace.DOWN); crl.onStartEndSet(e); - verify(player).sendMessage("parkour.set-the-end"); + checkSpigotMessage("parkour.set-the-end"); } /** @@ -657,4 +665,32 @@ public void testOnTeleportToFlagActionVisitorsChorusFruit() { verify(player, never()).setGameMode(GameMode.SURVIVAL); } + /** + * Check that spigot sent the message + * @param message - message to check + */ + public void checkSpigotMessage(String expectedMessage) { + checkSpigotMessage(expectedMessage, 1); + } + + public void checkSpigotMessage(String expectedMessage, int expectedOccurrences) { + // Capture the argument passed to spigot().sendMessage(...) if messages are sent + ArgumentCaptor captor = ArgumentCaptor.forClass(TextComponent.class); + + // Verify that sendMessage() was called at least 0 times (capture any sent messages) + verify(spigot, atLeast(0)).sendMessage(captor.capture()); + + // Get all captured TextComponents + List capturedMessages = captor.getAllValues(); + + // Count the number of occurrences of the expectedMessage in the captured messages + long actualOccurrences = capturedMessages.stream().map(component -> component.toLegacyText()) // Convert each TextComponent to plain text + .filter(messageText -> messageText.contains(expectedMessage)) // Check if the message contains the expected text + .count(); // Count how many times the expected message appears + + // Assert that the number of occurrences matches the expectedOccurrences + assertEquals("Expected message occurrence mismatch: " + expectedMessage, expectedOccurrences, + actualOccurrences); + } + } diff --git a/src/test/java/world/bentobox/parkour/mocks/ServerMocks.java b/src/test/java/world/bentobox/parkour/mocks/ServerMocks.java new file mode 100644 index 0000000..7f8b4b8 --- /dev/null +++ b/src/test/java/world/bentobox/parkour/mocks/ServerMocks.java @@ -0,0 +1,118 @@ +package world.bentobox.parkour.mocks; + +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import org.bukkit.Bukkit; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.Server; +import org.bukkit.Tag; +import org.bukkit.UnsafeValues; +import org.eclipse.jdt.annotation.NonNull; + +public final class ServerMocks { + + public static @NonNull Server newServer() { + Server mock = mock(Server.class); + + Logger noOp = mock(Logger.class); + when(mock.getLogger()).thenReturn(noOp); + when(mock.isPrimaryThread()).thenReturn(true); + + // Unsafe + UnsafeValues unsafe = mock(UnsafeValues.class); + when(mock.getUnsafe()).thenReturn(unsafe); + + // Server must be available before tags can be mocked. + Bukkit.setServer(mock); + + // Bukkit has a lot of static constants referencing registry values. To initialize those, the + // registries must be able to be fetched before the classes are touched. + Map, Object> registers = new HashMap<>(); + + doAnswer(invocationGetRegistry -> registers.computeIfAbsent(invocationGetRegistry.getArgument(0), clazz -> { + Registry registry = mock(Registry.class); + Map cache = new HashMap<>(); + doAnswer(invocationGetEntry -> { + NamespacedKey key = invocationGetEntry.getArgument(0); + // Some classes (like BlockType and ItemType) have extra generics that will be + // erased during runtime calls. To ensure accurate typing, grab the constant's field. + // This approach also allows us to return null for unsupported keys. + Class constantClazz; + try { + //noinspection unchecked + constantClazz = (Class) clazz + .getField(key.getKey().toUpperCase(Locale.ROOT).replace('.', '_')).getType(); + } catch (ClassCastException e) { + throw new RuntimeException(e); + } catch (NoSuchFieldException e) { + return null; + } + + return cache.computeIfAbsent(key, key1 -> { + Keyed keyed = mock(constantClazz); + doReturn(key).when(keyed).getKey(); + return keyed; + }); + }).when(registry).get(notNull()); + return registry; + })).when(mock).getRegistry(notNull()); + + // Tags are dependent on registries, but use a different method. + // This will set up blank tags for each constant; all that needs to be done to render them + // functional is to re-mock Tag#getValues. + doAnswer(invocationGetTag -> { + Tag tag = mock(Tag.class); + doReturn(invocationGetTag.getArgument(1)).when(tag).getKey(); + doReturn(Set.of()).when(tag).getValues(); + doAnswer(invocationIsTagged -> { + Keyed keyed = invocationIsTagged.getArgument(0); + Class type = invocationGetTag.getArgument(2); + if (!type.isAssignableFrom(keyed.getClass())) { + return null; + } + // Since these are mocks, the exact instance might not be equal. Consider equal keys equal. + return tag.getValues().contains(keyed) + || tag.getValues().stream().anyMatch(value -> value.getKey().equals(keyed.getKey())); + }).when(tag).isTagged(notNull()); + return tag; + }).when(mock).getTag(notNull(), notNull(), notNull()); + + // Once the server is all set up, touch BlockType and ItemType to initialize. + // This prevents issues when trying to access dependent methods from a Material constant. + try { + Class.forName("org.bukkit.inventory.ItemType"); + Class.forName("org.bukkit.block.BlockType"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + return mock; + } + + public static void unsetBukkitServer() { + try { + Field server = Bukkit.class.getDeclaredField("server"); + server.setAccessible(true); + server.set(null, null); + } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private ServerMocks() { + } + +} \ No newline at end of file