diff --git a/src/test/java/com/diamonddagger590/mccore/CorePluginMockBukkitTest.java b/src/test/java/com/diamonddagger590/mccore/CorePluginMockBukkitTest.java new file mode 100644 index 0000000..8f96887 --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/CorePluginMockBukkitTest.java @@ -0,0 +1,80 @@ +package com.diamonddagger590.mccore; + +import com.diamonddagger590.mccore.registry.RegistryAccess; +import com.diamonddagger590.mccore.testing.RegistryResetExtension; +import com.diamonddagger590.mccore.testing.TestCorePlugin; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +class CorePluginMockBukkitTest { + + private ServerMock server; + private TestCorePlugin plugin; + + @BeforeEach + void setUp() { + server = MockBukkit.mock(); + plugin = MockBukkit.load(TestCorePlugin.class); + RegistryResetExtension.setupRegistry(); + } + + @AfterEach + void tearDown() throws Exception { + RegistryResetExtension.resetRegistry(); + MockBukkit.unmock(); + Field instanceField = CorePlugin.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + instanceField.set(null, null); + } + + @Test + @DisplayName("Given a loaded plugin, when onEnable has run, then getInstance returns the plugin") + void getInstance_returnsPlugin_afterOnEnable() { + assertSame(plugin, CorePlugin.getInstance()); + } + + @Test + @DisplayName("Given a loaded plugin, when getMiniMessage is called, then returns non-null MiniMessage") + void getMiniMessage_returnsNonNull_afterOnEnable() { + MiniMessage miniMessage = plugin.getMiniMessage(); + assertNotNull(miniMessage); + } + + @Test + @DisplayName("Given a loaded plugin, when getMiniMessage is called, then returns the standard MiniMessage instance") + void getMiniMessage_returnsStandardInstance_afterOnEnable() { + MiniMessage miniMessage = plugin.getMiniMessage(); + assertSame(MiniMessage.miniMessage(), miniMessage); + } + + @Test + @DisplayName("Given a loaded plugin, when registryAccess is called, then returns the RegistryAccess singleton") + void registryAccess_returnsSingleton_afterOnEnable() { + RegistryAccess fromPlugin = plugin.registryAccess(); + assertNotNull(fromPlugin); + assertSame(RegistryAccess.registryAccess(), fromPlugin); + } + + @Test + @DisplayName("Given a loaded plugin, when onDisable is called, then no exception is thrown") + void onDisable_doesNotThrow_whenCalled() { + assertDoesNotThrow(() -> plugin.onDisable()); + } + + @Test + @DisplayName("Given a loaded plugin, when getTimeProvider is called, then returns non-null TimeProvider") + void getTimeProvider_returnsNonNull_afterOnEnable() { + assertNotNull(plugin.getTimeProvider()); + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/database/DatabaseTest.java b/src/test/java/com/diamonddagger590/mccore/database/DatabaseTest.java index 199e20f..77805dd 100644 --- a/src/test/java/com/diamonddagger590/mccore/database/DatabaseTest.java +++ b/src/test/java/com/diamonddagger590/mccore/database/DatabaseTest.java @@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -396,6 +397,63 @@ void initializeDatabase_configuresDataSourceWithConnectionDetails_whenDriverIsRe } } + @Test + @DisplayName("Given a configured data source that throws SQLException, when getConnection is called, then wraps in RuntimeException") + void getConnection_throwsRuntimeException_whenDataSourceThrowsSQLException() { + RegistryResetExtension.setupRegistry(); + try { + DriverRegistry driverRegistry = new DriverRegistry(); + RegistryAccess.registryAccess().register(driverRegistry); + + DatabaseDriver mockDriver = mock(DatabaseDriver.class); + when(mockDriver.tryDriver()).thenReturn(true); + when(mockDriver.getDriverType()).thenReturn(DatabaseDriverType.SQLITE); + when(mockDriver.getDatabaseDriverClass()).thenReturn("org.sqlite.JDBC"); + when(mockDriver.getConnectionUrl(any())).thenReturn("jdbc:sqlite::memory:"); + when(mockDriver.getDataSourceProperties()).thenReturn(List.of()); + driverRegistry.register(mockDriver); + + when(mockPlugin.registryAccess()).thenReturn(RegistryAccess.registryAccess()); + + TestDatabase testDb = new TestDatabase(mockPlugin, DatabaseDriverType.SQLITE); + + HikariDataSource mockDataSource = mock(HikariDataSource.class); + try { + when(mockDataSource.getConnection()).thenThrow(new SQLException("Pool exhausted")); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + try { + var field = Database.class.getDeclaredField("dataSource"); + field.setAccessible(true); + field.set(testDb, mockDataSource); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + + RuntimeException ex = assertThrows(RuntimeException.class, testDb::getConnection); + assertInstanceOf(SQLException.class, ex.getCause()); + assertEquals("Pool exhausted", ex.getCause().getMessage()); + + testDb.getDatabaseExecutorService().shutdownNow(); + } finally { + RegistryResetExtension.resetRegistry(); + } + } + + @Test + @DisplayName("Given getTables throws SQLException inside tableExists lambda, when checking tableExists, then returns false") + void tableExists_returnsFalse_whenGetTablesThrowsSQLException() throws SQLException { + Connection mockConnection = mock(Connection.class); + DatabaseMetaData mockMetaData = mock(DatabaseMetaData.class); + + when(mockConnection.getMetaData()).thenReturn(mockMetaData); + when(mockMetaData.getTables(null, null, null, new String[]{"TABLE"})).thenThrow(new SQLException("getTables failed")); + + assertFalse(database.tableExists(mockConnection, "any_table")); + } + static class TestDatabase extends Database { private static final Credentials CREDENTIALS = new Credentials( diff --git a/src/test/java/com/diamonddagger590/mccore/gui/GuiManagerTest.java b/src/test/java/com/diamonddagger590/mccore/gui/GuiManagerTest.java index 9c4f272..65a7ebc 100644 --- a/src/test/java/com/diamonddagger590/mccore/gui/GuiManagerTest.java +++ b/src/test/java/com/diamonddagger590/mccore/gui/GuiManagerTest.java @@ -1,8 +1,10 @@ package com.diamonddagger590.mccore.gui; import com.diamonddagger590.mccore.CorePlugin; +import com.diamonddagger590.mccore.event.gui.CoreGuiOpenEvent; import com.diamonddagger590.mccore.player.CorePlayer; import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.plugin.PluginManager; @@ -10,6 +12,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.jetbrains.annotations.NotNull; import java.lang.reflect.Field; import java.util.Optional; @@ -19,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; @@ -330,4 +336,105 @@ void trackPlayerGui_firesEvent_always() { verify(mockPluginManager).callEvent(any()); } + + @SuppressWarnings("unchecked") + private & KeyedGui> T createMockKeyedGui(@NotNull NamespacedKey key) { + T gui = (T) mock(Gui.class, org.mockito.Mockito.withSettings().extraInterfaces(KeyedGui.class)); + when(gui.getUUID()).thenReturn(UUID.randomUUID()); + when(((KeyedGui) gui).getGuiKey()).thenReturn(Optional.of(key)); + return gui; + } + + @Test + @DisplayName("Given a KeyedGui, when trackPlayerGui, then CoreGuiOpenEvent contains the gui key") + void trackPlayerGui_eventContainsGuiKey_whenGuiIsKeyed() { + UUID playerUUID = UUID.randomUUID(); + NamespacedKey expectedKey = new NamespacedKey("test", "my_gui"); + Gui gui = createMockKeyedGui(expectedKey); + + guiManager.trackPlayerGui(playerUUID, gui); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CoreGuiOpenEvent.class); + verify(mockPluginManager).callEvent(captor.capture()); + CoreGuiOpenEvent event = captor.getValue(); + + assertEquals(playerUUID, event.getPlayerUUID()); + assertTrue(event.getGuiKey().isPresent()); + assertEquals(expectedKey, event.getGuiKey().get()); + } + + @Test + @DisplayName("Given a non-KeyedGui, when trackPlayerGui, then CoreGuiOpenEvent has empty gui key") + void trackPlayerGui_eventHasEmptyKey_whenGuiIsNotKeyed() { + UUID playerUUID = UUID.randomUUID(); + Gui gui = createMockGui(); + + guiManager.trackPlayerGui(playerUUID, gui); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CoreGuiOpenEvent.class); + verify(mockPluginManager).callEvent(captor.capture()); + CoreGuiOpenEvent event = captor.getValue(); + + assertFalse(event.getGuiKey().isPresent()); + } + + @Test + @DisplayName("Given a gui with multiple online viewers, when refreshGui, then updates inventory for all") + void refreshGui_updatesAllViewers_whenMultipleOnline() { + UUID player1UUID = UUID.randomUUID(); + UUID player2UUID = UUID.randomUUID(); + UUID player3UUID = UUID.randomUUID(); + Gui gui = createMockGui(); + Player onlinePlayer1 = mock(Player.class); + Player onlinePlayer2 = mock(Player.class); + Player onlinePlayer3 = mock(Player.class); + + guiManager.trackPlayerGui(player1UUID, gui); + guiManager.trackPlayerGui(player2UUID, gui); + guiManager.trackPlayerGui(player3UUID, gui); + + when(mockServer.getPlayer(player1UUID)).thenReturn(onlinePlayer1); + when(mockServer.getPlayer(player2UUID)).thenReturn(onlinePlayer2); + when(mockServer.getPlayer(player3UUID)).thenReturn(onlinePlayer3); + + guiManager.refreshGui(gui); + + verify(onlinePlayer1).updateInventory(); + verify(onlinePlayer2).updateInventory(); + verify(onlinePlayer3).updateInventory(); + } + + @Test + @DisplayName("Given a gui with mixed online/offline viewers, when refreshGui, then only updates online viewers") + void refreshGui_updatesOnlyOnlineViewers_whenMixedOnlineOffline() { + UUID onlineUUID = UUID.randomUUID(); + UUID offlineUUID = UUID.randomUUID(); + Gui gui = createMockGui(); + Player onlinePlayer = mock(Player.class); + + guiManager.trackPlayerGui(onlineUUID, gui); + guiManager.trackPlayerGui(offlineUUID, gui); + + when(mockServer.getPlayer(onlineUUID)).thenReturn(onlinePlayer); + when(mockServer.getPlayer(offlineUUID)).thenReturn(null); + + guiManager.refreshGui(gui); + + verify(onlinePlayer).updateInventory(); + } + + @Test + @DisplayName("Given trackPlayerGui fired event, then event carries the correct gui instance") + void trackPlayerGui_eventCarriesGuiInstance_always() { + UUID playerUUID = UUID.randomUUID(); + Gui gui = createMockGui(); + + guiManager.trackPlayerGui(playerUUID, gui); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CoreGuiOpenEvent.class); + verify(mockPluginManager).callEvent(captor.capture()); + CoreGuiOpenEvent event = captor.getValue(); + + assertSame(gui, event.getGui()); + } }