diff --git a/src/test/java/com/diamonddagger590/mccore/builder/item/ItemPluginTypeGetCustomItemTest.java b/src/test/java/com/diamonddagger590/mccore/builder/item/ItemPluginTypeGetCustomItemTest.java new file mode 100644 index 0000000..007231f --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/builder/item/ItemPluginTypeGetCustomItemTest.java @@ -0,0 +1,316 @@ +package com.diamonddagger590.mccore.builder.item; + +import com.diamonddagger590.mccore.external.itemsadder.CoreItemsAdderHook; +import com.diamonddagger590.mccore.external.nexo.CoreNexoHook; +import com.diamonddagger590.mccore.registry.RegistryAccess; +import com.diamonddagger590.mccore.registry.RegistryKey; +import com.diamonddagger590.mccore.registry.plugin.CorePluginHookKey; +import com.diamonddagger590.mccore.registry.plugin.PluginHookRegistry; +import com.diamonddagger590.mccore.testing.RegistryResetExtension; +import com.diamonddagger590.mccore.testing.TestCorePlugin; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ItemPluginTypeGetCustomItemTest { + + private TestCorePlugin plugin; + + @BeforeEach + void setUp() { + MockBukkit.mock(); + plugin = MockBukkit.load(TestCorePlugin.class); + RegistryResetExtension.setupRegistry(); + } + + @AfterEach + void tearDown() { + RegistryResetExtension.resetRegistry(); + MockBukkit.unmock(); + } + + private PluginHookRegistry getPluginHookRegistry() { + return plugin.registryAccess().registry(RegistryKey.PLUGIN_HOOK); + } + + @Nested + @DisplayName("NEXO getCustomItem") + class NexoTests { + + @Test + @DisplayName("Given Nexo hook with matching item, when getCustomItem called, then returns hook's item") + void getCustomItem_returnsHookItem_whenNexoHookReturnsItem() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreNexoHook mockNexoHook = mock(CoreNexoHook.class); + ItemStack expectedItem = ItemType.DIAMOND_SWORD.createItemStack(1); + when(mockNexoHook.item("custom_sword")).thenReturn(Optional.of(expectedItem)); + hookRegistry.register(mockNexoHook); + + ItemStack result = ItemPluginType.NEXO.getCustomItem("custom_sword"); + + assertEquals(expectedItem, result); + } + + @Test + @DisplayName("Given Nexo hook with no matching item and valid ItemType, when getCustomItem called, then returns ItemType item") + void getCustomItem_returnsItemTypeItem_whenNexoHookReturnsEmptyAndItemTypeValid() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreNexoHook mockNexoHook = mock(CoreNexoHook.class); + when(mockNexoHook.item("stone")).thenReturn(Optional.empty()); + hookRegistry.register(mockNexoHook); + + ItemStack result = ItemPluginType.NEXO.getCustomItem("stone"); + + assertNotNull(result); + assertEquals(ItemType.STONE, result.getType().asItemType()); + } + + @Test + @DisplayName("Given no Nexo hook and valid ItemType, when getCustomItem called, then returns ItemType item") + void getCustomItem_returnsItemTypeItem_whenNoNexoHookAndItemTypeValid() { + ItemStack result = ItemPluginType.NEXO.getCustomItem("diamond"); + + assertNotNull(result); + assertEquals(ItemType.DIAMOND, result.getType().asItemType()); + } + + @Test + @DisplayName("Given no Nexo hook and invalid item name, when getCustomItem called, then falls back to STONE") + void getCustomItem_fallsBackToStone_whenNoHookAndInvalidItemName() { + ItemStack result = ItemPluginType.NEXO.getCustomItem("not_a_real_item_xyz_123"); + + assertNotNull(result); + assertEquals(ItemType.STONE, result.getType().asItemType()); + } + + @Test + @DisplayName("Given Nexo hook returning empty and invalid item name, when getCustomItem called, then falls back to STONE") + void getCustomItem_fallsBackToStone_whenHookEmptyAndInvalidItemName() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreNexoHook mockNexoHook = mock(CoreNexoHook.class); + when(mockNexoHook.item(anyString())).thenReturn(Optional.empty()); + hookRegistry.register(mockNexoHook); + + ItemStack result = ItemPluginType.NEXO.getCustomItem("nonexistent_garbage_xyz"); + + assertNotNull(result); + assertEquals(ItemType.STONE, result.getType().asItemType()); + } + } + + @Nested + @DisplayName("ITEMS_ADDER getCustomItem") + class ItemsAdderTests { + + @Test + @DisplayName("Given ItemsAdder hook with matching item, when getCustomItem called, then returns hook's item") + void getCustomItem_returnsHookItem_whenItemsAdderHookReturnsItem() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreItemsAdderHook mockHook = mock(CoreItemsAdderHook.class); + ItemStack expectedItem = ItemType.IRON_INGOT.createItemStack(1); + when(mockHook.item("custom_ingot")).thenReturn(Optional.of(expectedItem)); + hookRegistry.register(mockHook); + + ItemStack result = ItemPluginType.ITEMS_ADDER.getCustomItem("custom_ingot"); + + assertEquals(expectedItem, result); + } + + @Test + @DisplayName("Given ItemsAdder hook with no matching item and valid ItemType, when getCustomItem called, then returns ItemType item") + void getCustomItem_returnsItemTypeItem_whenItemsAdderHookReturnsEmptyAndItemTypeValid() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreItemsAdderHook mockHook = mock(CoreItemsAdderHook.class); + when(mockHook.item("oak_log")).thenReturn(Optional.empty()); + hookRegistry.register(mockHook); + + ItemStack result = ItemPluginType.ITEMS_ADDER.getCustomItem("oak_log"); + + assertNotNull(result); + assertEquals(ItemType.OAK_LOG, result.getType().asItemType()); + } + + @Test + @DisplayName("Given no ItemsAdder hook and valid ItemType, when getCustomItem called, then returns ItemType item") + void getCustomItem_returnsItemTypeItem_whenNoItemsAdderHookAndItemTypeValid() { + ItemStack result = ItemPluginType.ITEMS_ADDER.getCustomItem("iron_sword"); + + assertNotNull(result); + assertEquals(ItemType.IRON_SWORD, result.getType().asItemType()); + } + + @Test + @DisplayName("Given no ItemsAdder hook and invalid item name, when getCustomItem called, then falls back to STONE") + void getCustomItem_fallsBackToStone_whenNoHookAndInvalidItemName() { + ItemStack result = ItemPluginType.ITEMS_ADDER.getCustomItem("fake_item_xyz_999"); + + assertNotNull(result); + assertEquals(ItemType.STONE, result.getType().asItemType()); + } + + @Test + @DisplayName("Given ItemsAdder hook returning empty and invalid item name, when getCustomItem called, then falls back to STONE") + void getCustomItem_fallsBackToStone_whenHookEmptyAndInvalidItemName() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreItemsAdderHook mockHook = mock(CoreItemsAdderHook.class); + when(mockHook.item(anyString())).thenReturn(Optional.empty()); + hookRegistry.register(mockHook); + + ItemStack result = ItemPluginType.ITEMS_ADDER.getCustomItem("nonexistent_garbage_abc"); + + assertNotNull(result); + assertEquals(ItemType.STONE, result.getType().asItemType()); + } + } + + @Nested + @DisplayName("NONE getCustomItem") + class NoneTests { + + @Test + @DisplayName("Given Nexo hook recognizes item and returns it, when getCustomItem called, then returns Nexo item") + void getCustomItem_returnsNexoItem_whenNexoHookRecognizesItem() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreNexoHook mockNexoHook = mock(CoreNexoHook.class); + ItemStack expectedItem = ItemType.GOLDEN_APPLE.createItemStack(1); + when(mockNexoHook.isItem("nexo_apple")).thenReturn(true); + when(mockNexoHook.item("nexo_apple")).thenReturn(Optional.of(expectedItem)); + hookRegistry.register(mockNexoHook); + + ItemStack result = ItemPluginType.NONE.getCustomItem("nexo_apple"); + + assertEquals(expectedItem, result); + } + + @Test + @DisplayName("Given Nexo hook recognizes item but returns empty optional, when getCustomItem called, then falls through to ItemType") + void getCustomItem_fallsThroughToItemType_whenNexoHookReturnsEmptyItem() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreNexoHook mockNexoHook = mock(CoreNexoHook.class); + when(mockNexoHook.isItem("diamond")).thenReturn(true); + when(mockNexoHook.item("diamond")).thenReturn(Optional.empty()); + hookRegistry.register(mockNexoHook); + + ItemStack result = ItemPluginType.NONE.getCustomItem("diamond"); + + assertNotNull(result); + assertEquals(ItemType.DIAMOND, result.getType().asItemType()); + } + + @Test + @DisplayName("Given Nexo hook does not recognize item but ItemsAdder does, when getCustomItem called, then returns ItemsAdder item") + void getCustomItem_returnsItemsAdderItem_whenNexoDoesNotRecognizeButItemsAdderDoes() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreNexoHook mockNexoHook = mock(CoreNexoHook.class); + when(mockNexoHook.isItem("ia_item")).thenReturn(false); + hookRegistry.register(mockNexoHook); + + CoreItemsAdderHook mockIaHook = mock(CoreItemsAdderHook.class); + ItemStack expectedItem = ItemType.EMERALD.createItemStack(1); + when(mockIaHook.isItem("ia_item")).thenReturn(true); + when(mockIaHook.item("ia_item")).thenReturn(Optional.of(expectedItem)); + hookRegistry.register(mockIaHook); + + ItemStack result = ItemPluginType.NONE.getCustomItem("ia_item"); + + assertEquals(expectedItem, result); + } + + @Test + @DisplayName("Given ItemsAdder recognizes item but returns empty optional, when getCustomItem called, then falls through to ItemType") + void getCustomItem_fallsThroughToItemType_whenItemsAdderReturnsEmptyItem() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreItemsAdderHook mockIaHook = mock(CoreItemsAdderHook.class); + when(mockIaHook.isItem("gold_ingot")).thenReturn(true); + when(mockIaHook.item("gold_ingot")).thenReturn(Optional.empty()); + hookRegistry.register(mockIaHook); + + ItemStack result = ItemPluginType.NONE.getCustomItem("gold_ingot"); + + assertNotNull(result); + assertEquals(ItemType.GOLD_INGOT, result.getType().asItemType()); + } + + @Test + @DisplayName("Given no hooks registered and valid ItemType, when getCustomItem called, then returns ItemType item") + void getCustomItem_returnsItemTypeItem_whenNoHooksAndItemTypeValid() { + ItemStack result = ItemPluginType.NONE.getCustomItem("stick"); + + assertNotNull(result); + assertEquals(ItemType.STICK, result.getType().asItemType()); + } + + @Test + @DisplayName("Given no hooks registered and invalid item name, when getCustomItem called, then falls back to STONE") + void getCustomItem_fallsBackToStone_whenNoHooksAndInvalidItemName() { + ItemStack result = ItemPluginType.NONE.getCustomItem("completely_invalid_item_xyz"); + + assertNotNull(result); + assertEquals(ItemType.STONE, result.getType().asItemType()); + } + + @Test + @DisplayName("Given both hooks present but neither recognizes item and valid ItemType, when getCustomItem called, then returns ItemType item") + void getCustomItem_returnsItemTypeItem_whenBothHooksPresentButNeitherRecognizesAndItemTypeValid() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreNexoHook mockNexoHook = mock(CoreNexoHook.class); + when(mockNexoHook.isItem("coal")).thenReturn(false); + hookRegistry.register(mockNexoHook); + + CoreItemsAdderHook mockIaHook = mock(CoreItemsAdderHook.class); + when(mockIaHook.isItem("coal")).thenReturn(false); + hookRegistry.register(mockIaHook); + + ItemStack result = ItemPluginType.NONE.getCustomItem("coal"); + + assertNotNull(result); + assertEquals(ItemType.COAL, result.getType().asItemType()); + } + + @Test + @DisplayName("Given both hooks present but neither recognizes item and invalid name, when getCustomItem called, then falls back to STONE") + void getCustomItem_fallsBackToStone_whenBothHooksPresentNeitherRecognizesAndInvalidName() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreNexoHook mockNexoHook = mock(CoreNexoHook.class); + when(mockNexoHook.isItem("gibberish_abc")).thenReturn(false); + hookRegistry.register(mockNexoHook); + + CoreItemsAdderHook mockIaHook = mock(CoreItemsAdderHook.class); + when(mockIaHook.isItem("gibberish_abc")).thenReturn(false); + hookRegistry.register(mockIaHook); + + ItemStack result = ItemPluginType.NONE.getCustomItem("gibberish_abc"); + + assertNotNull(result); + assertEquals(ItemType.STONE, result.getType().asItemType()); + } + + @Test + @DisplayName("Given only ItemsAdder hook registered and it recognizes item, when getCustomItem called, then returns ItemsAdder item") + void getCustomItem_returnsItemsAdderItem_whenOnlyItemsAdderRegisteredAndRecognizes() { + PluginHookRegistry hookRegistry = getPluginHookRegistry(); + CoreItemsAdderHook mockIaHook = mock(CoreItemsAdderHook.class); + ItemStack expectedItem = ItemType.REDSTONE.createItemStack(1); + when(mockIaHook.isItem("custom_redstone")).thenReturn(true); + when(mockIaHook.item("custom_redstone")).thenReturn(Optional.of(expectedItem)); + hookRegistry.register(mockIaHook); + + ItemStack result = ItemPluginType.NONE.getCustomItem("custom_redstone"); + + assertEquals(expectedItem, result); + } + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/database/DatabaseNonBlockingInitTest.java b/src/test/java/com/diamonddagger590/mccore/database/DatabaseNonBlockingInitTest.java new file mode 100644 index 0000000..95682d6 --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/database/DatabaseNonBlockingInitTest.java @@ -0,0 +1,270 @@ +package com.diamonddagger590.mccore.database; + +import com.diamonddagger590.mccore.CorePlugin; +import com.diamonddagger590.mccore.database.driver.DatabaseDriver; +import com.diamonddagger590.mccore.database.driver.DatabaseDriverType; +import com.diamonddagger590.mccore.database.driver.DriverRegistry; +import com.diamonddagger590.mccore.event.database.TablesUpdatedEvent; +import com.diamonddagger590.mccore.registry.RegistryAccess; +import com.diamonddagger590.mccore.registry.RegistryKey; +import com.diamonddagger590.mccore.testing.RegistryResetExtension; +import com.diamonddagger590.mccore.testing.TestCorePlugin; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +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 java.sql.Connection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class DatabaseNonBlockingInitTest { + + private TestCorePlugin plugin; + + @BeforeEach + void setUp() { + MockBukkit.mock(); + plugin = MockBukkit.load(TestCorePlugin.class); + RegistryResetExtension.setupRegistry(); + } + + @AfterEach + void tearDown() { + RegistryResetExtension.resetRegistry(); + MockBukkit.unmock(); + } + + private void setupDriverRegistry() { + 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); + } + + private CompletableFuture registerTablesUpdatedListener() { + CompletableFuture done = new CompletableFuture<>(); + Bukkit.getPluginManager().registerEvents(new Listener() { + @EventHandler + public void onTablesUpdated(TablesUpdatedEvent event) { + done.complete(null); + } + }, plugin); + return done; + } + + @Test + @DisplayName("Given blockMainThreadOnStart returns false, when initializeDatabase called, then tables are created asynchronously") + void initializeDatabase_createsTablesAsync_whenNonBlocking() throws Exception { + setupDriverRegistry(); + + CompletableFuture initDone = registerTablesUpdatedListener(); + + AsyncTestDatabase database = new AsyncTestDatabase(plugin); + try { + database.initializeDatabase(); + + initDone.get(10, TimeUnit.SECONDS); + + Connection conn = database.getConnection(); + boolean exists = database.tableExists(conn, "table_history"); + conn.close(); + assertTrue(exists); + } finally { + database.shutdown(); + } + } + + @Test + @DisplayName("Given blockMainThreadOnStart returns false with custom functions, when initializeDatabase called, then custom functions are invoked asynchronously") + void initializeDatabase_invokesCustomFunctionsAsync_whenNonBlocking() throws Exception { + setupDriverRegistry(); + + AsyncTestDatabase database = new AsyncTestDatabase(plugin); + CompletableFuture createDone = new CompletableFuture<>(); + CompletableFuture updateDone = new CompletableFuture<>(); + + database.addCreateTableFunction(db -> { + CompletableFuture future = new CompletableFuture<>(); + db.getDatabaseExecutorService().submit(() -> { + createDone.complete(null); + future.complete(null); + }); + return future; + }); + database.addUpdateTableFunction(db -> { + CompletableFuture future = new CompletableFuture<>(); + db.getDatabaseExecutorService().submit(() -> { + updateDone.complete(null); + future.complete(null); + }); + return future; + }); + + try { + database.initializeDatabase(); + + createDone.get(10, TimeUnit.SECONDS); + updateDone.get(10, TimeUnit.SECONDS); + } finally { + database.shutdown(); + } + } + + @Test + @DisplayName("Given blockMainThreadOnStart returns false, when initializeDatabase called, then method returns before tables are fully created") + void initializeDatabase_returnsImmediately_whenNonBlocking() throws Exception { + setupDriverRegistry(); + + CompletableFuture initDone = registerTablesUpdatedListener(); + + AsyncTestDatabase database = new AsyncTestDatabase(plugin); + try { + long start = System.nanoTime(); + database.initializeDatabase(); + long elapsed = System.nanoTime() - start; + + assertTrue(elapsed < TimeUnit.SECONDS.toNanos(2), + "Non-blocking initializeDatabase should return quickly"); + } finally { + initDone.get(10, TimeUnit.SECONDS); + database.shutdown(); + } + } + + @Test + @DisplayName("Given blockMainThreadOnStart returns false with multiple create functions, when initializeDatabase called, then all functions are invoked") + void initializeDatabase_invokesAllCreateFunctions_whenNonBlockingWithMultiple() throws Exception { + setupDriverRegistry(); + + AsyncTestDatabase database = new AsyncTestDatabase(plugin); + CompletableFuture create1Done = new CompletableFuture<>(); + CompletableFuture create2Done = new CompletableFuture<>(); + + database.addCreateTableFunction(db -> { + CompletableFuture future = new CompletableFuture<>(); + db.getDatabaseExecutorService().submit(() -> { + create1Done.complete(null); + future.complete(null); + }); + return future; + }); + database.addCreateTableFunction(db -> { + CompletableFuture future = new CompletableFuture<>(); + db.getDatabaseExecutorService().submit(() -> { + create2Done.complete(null); + future.complete(null); + }); + return future; + }); + + try { + database.initializeDatabase(); + + create1Done.get(10, TimeUnit.SECONDS); + create2Done.get(10, TimeUnit.SECONDS); + } finally { + database.shutdown(); + } + } + + @Test + @DisplayName("Given blockMainThreadOnStart returns false with multiple update functions, when initializeDatabase called, then all update functions are invoked") + void initializeDatabase_invokesAllUpdateFunctions_whenNonBlockingWithMultiple() throws Exception { + setupDriverRegistry(); + + AsyncTestDatabase database = new AsyncTestDatabase(plugin); + CompletableFuture update1Done = new CompletableFuture<>(); + CompletableFuture update2Done = new CompletableFuture<>(); + + database.addUpdateTableFunction(db -> { + CompletableFuture future = new CompletableFuture<>(); + db.getDatabaseExecutorService().submit(() -> { + update1Done.complete(null); + future.complete(null); + }); + return future; + }); + database.addUpdateTableFunction(db -> { + CompletableFuture future = new CompletableFuture<>(); + db.getDatabaseExecutorService().submit(() -> { + update2Done.complete(null); + future.complete(null); + }); + return future; + }); + + try { + database.initializeDatabase(); + + update1Done.get(10, TimeUnit.SECONDS); + update2Done.get(10, TimeUnit.SECONDS); + } finally { + database.shutdown(); + } + } + + @Test + @DisplayName("Given blockMainThreadOnStart returns false, when initializeDatabase called, then connection can be obtained after async init completes") + void getConnection_returnsValidConnection_whenNonBlockingInitCompletes() throws Exception { + setupDriverRegistry(); + + CompletableFuture initDone = registerTablesUpdatedListener(); + + AsyncTestDatabase database = new AsyncTestDatabase(plugin); + try { + database.initializeDatabase(); + + initDone.get(10, TimeUnit.SECONDS); + + Connection conn = database.getConnection(); + assertNotNull(conn); + assertFalse(conn.isClosed()); + conn.close(); + } finally { + database.shutdown(); + } + } + + static class AsyncTestDatabase extends Database { + private static final Credentials CREDENTIALS = new Credentials("", 0, "", "", ""); + private static final ConnectionDetails CONNECTION_DETAILS = new ConnectionDetails(5000, 300000, 600000, 2, 10, 0); + + AsyncTestDatabase(CorePlugin plugin) { + super(plugin, DatabaseDriverType.SQLITE); + } + + @Override + protected Credentials getCredentials() { + return CREDENTIALS; + } + + @Override + protected ConnectionDetails getConnectionDetails() { + return CONNECTION_DETAILS; + } + + @Override + protected boolean blockMainThreadOnStart() { + return false; + } + } +}