From 8885f03d67871f330b868c34c21630560bb7f257 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 09:34:40 +0000 Subject: [PATCH] Add hook-interaction and fallback tests for Custom*Wrapper classes Cover previously-untested methods across CustomBlockWrapper, CustomItemWrapper, and CustomEntityWrapper: - Constructor(Block/ItemStack/Entity): 5 tests each covering no-hooks, hook-with-models, hook-with-empty-models, hook-not-recognizing, and hook-returning-empty-optional paths. - blockName/itemName/entityName vanilla branch: MockBukkit-wrapped tests verifying the tag output from translationKey(). - blockName/itemName/entityName hook-doesn't-recognize fallback. - equals(Block/Entity): 5 tests each covering material match/mismatch, hook confirms/denies, and no-hooks-registered paths. - itemBuilder: 3 tests each for vanilla, custom-no-hooks, and custom-with-hook paths (MockBukkit + CorePlugin mock lifecycle). - customModels (static): 3 tests each for no-hooks, hook-with-models, and hook-with-empty-optional paths. - drops: hook-provides-drops and vanilla-fallback (block.getDrops) paths. - removeBlock: hook-delegates and no-hooks-sets-AIR paths. - playBlockDropEffects: hook-delegates and vanilla-fallback (sound + particle) paths. Co-Authored-By: Claude --- .../util/item/CustomBlockWrapperTest.java | 557 ++++++++++++++++++ .../util/item/CustomEntityWrapperTest.java | 273 +++++++++ .../util/item/CustomItemWrapperTest.java | 267 +++++++++ 3 files changed, 1097 insertions(+) diff --git a/src/test/java/com/diamonddagger590/mccore/util/item/CustomBlockWrapperTest.java b/src/test/java/com/diamonddagger590/mccore/util/item/CustomBlockWrapperTest.java index dc8624c..5ddce41 100644 --- a/src/test/java/com/diamonddagger590/mccore/util/item/CustomBlockWrapperTest.java +++ b/src/test/java/com/diamonddagger590/mccore/util/item/CustomBlockWrapperTest.java @@ -1,14 +1,22 @@ package com.diamonddagger590.mccore.util.item; import com.diamonddagger590.mccore.CorePlugin; +import com.diamonddagger590.mccore.builder.item.ItemPluginType; import com.diamonddagger590.mccore.external.common.CustomBlockHook; +import com.diamonddagger590.mccore.external.common.CustomItemHook; import com.diamonddagger590.mccore.registry.RegistryAccess; import com.diamonddagger590.mccore.registry.RegistryKey; import com.diamonddagger590.mccore.registry.plugin.PluginHook; import com.diamonddagger590.mccore.testing.RegistryResetExtension; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.SoundGroup; +import org.bukkit.World; import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; import org.bukkit.entity.Entity; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -18,6 +26,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockito.MockedStatic; import java.lang.reflect.Field; import java.util.List; @@ -27,7 +37,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; +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.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class CustomBlockWrapperTest { @@ -411,6 +431,364 @@ void blockName_returnsHookProvidedName_whenHookIsRegistered() throws Exception { CustomBlockWrapper wrapper = customBlockWrapper("nexo:ruby_ore"); assertEquals("Ruby Ore", wrapper.blockName()); } + + @Test + @DisplayName("Given vanilla material wrapper, when getting blockName, then returns lang tag") + void blockName_returnsLangTag_whenVanillaMaterial() { + MockBukkit.mock(); + try { + CustomBlockWrapper wrapper = materialWrapper(Material.STONE); + String name = wrapper.blockName(); + assertTrue(name.startsWith(""), "Expected lang tag to end with > but got: " + name); + } finally { + MockBukkit.unmock(); + } + } + + @Test + @DisplayName("Given custom block with hook that doesn't recognize it, when getting blockName, then returns Missing Block") + void blockName_returnsMissingBlock_whenHookDoesNotRecognize() throws Exception { + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(new TestCustomBlockPluginHook()); + CustomBlockWrapper wrapper = customBlockWrapper("nexo:unknown_block"); + assertEquals("Missing Block", wrapper.blockName()); + } + } + + @Nested + @DisplayName("Constructor(Block)") + class BlockConstructor { + + @Test + @DisplayName("Given a block with no hooks registered, when constructing, then uses block material") + void constructor_usesMaterial_whenNoHooksRegistered() { + Block mockBlock = mock(Block.class); + when(mockBlock.getType()).thenReturn(Material.IRON_ORE); + + CustomBlockWrapper wrapper = new CustomBlockWrapper(mockBlock); + + assertTrue(wrapper.isVanilla()); + assertFalse(wrapper.isCustom()); + assertTrue(wrapper.material().isPresent()); + assertEquals(Material.IRON_ORE, wrapper.material().get()); + } + + @Test + @DisplayName("Given a block with hook that recognizes it and returns models, when constructing, then uses custom block") + void constructor_usesCustomBlock_whenHookReturnsModels() { + Block mockBlock = mock(Block.class); + ConfigurableBlockHook hook = new ConfigurableBlockHook(true, Optional.of(Set.of("nexo:ruby_ore"))); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomBlockWrapper wrapper = new CustomBlockWrapper(mockBlock); + + assertTrue(wrapper.isCustom()); + assertFalse(wrapper.isVanilla()); + assertTrue(wrapper.customBlock().isPresent()); + assertEquals("nexo:ruby_ore", wrapper.customBlock().get()); + } + + @Test + @DisplayName("Given a block with hook that recognizes it but returns empty models, when constructing, then uses block material") + void constructor_usesMaterial_whenHookReturnsEmptyModels() { + Block mockBlock = mock(Block.class); + when(mockBlock.getType()).thenReturn(Material.STONE); + ConfigurableBlockHook hook = new ConfigurableBlockHook(true, Optional.of(Set.of())); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomBlockWrapper wrapper = new CustomBlockWrapper(mockBlock); + + assertTrue(wrapper.isVanilla()); + assertEquals(Material.STONE, wrapper.material().get()); + } + + @Test + @DisplayName("Given a block with hook that does not recognize it, when constructing, then uses block material") + void constructor_usesMaterial_whenHookDoesNotRecognize() { + Block mockBlock = mock(Block.class); + when(mockBlock.getType()).thenReturn(Material.DIRT); + ConfigurableBlockHook hook = new ConfigurableBlockHook(false, Optional.empty()); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomBlockWrapper wrapper = new CustomBlockWrapper(mockBlock); + + assertTrue(wrapper.isVanilla()); + assertEquals(Material.DIRT, wrapper.material().get()); + } + + @Test + @DisplayName("Given a block with hook that returns models with no results optional, when constructing, then uses block material") + void constructor_usesMaterial_whenHookReturnsEmptyOptional() { + Block mockBlock = mock(Block.class); + when(mockBlock.getType()).thenReturn(Material.GOLD_ORE); + ConfigurableBlockHook hook = new ConfigurableBlockHook(true, Optional.empty()); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomBlockWrapper wrapper = new CustomBlockWrapper(mockBlock); + + assertTrue(wrapper.isVanilla()); + assertEquals(Material.GOLD_ORE, wrapper.material().get()); + } + } + + @Nested + @DisplayName("equals(Block)") + class EqualsBlock { + + @Test + @DisplayName("Given vanilla wrapper and block with matching material, when comparing, then returns true") + void equalsBlock_returnsTrue_whenMaterialMatches() { + Block mockBlock = mock(Block.class); + when(mockBlock.getType()).thenReturn(Material.STONE); + CustomBlockWrapper wrapper = materialWrapper(Material.STONE); + + assertTrue(wrapper.equals(mockBlock)); + } + + @Test + @DisplayName("Given vanilla wrapper and block with different material, when comparing, then returns false") + void equalsBlock_returnsFalse_whenMaterialDiffers() { + Block mockBlock = mock(Block.class); + when(mockBlock.getType()).thenReturn(Material.DIRT); + CustomBlockWrapper wrapper = materialWrapper(Material.STONE); + + assertFalse(wrapper.equals(mockBlock)); + } + + @Test + @DisplayName("Given custom wrapper with hook that confirms type match, when comparing, then returns true") + void equalsBlock_returnsTrue_whenHookConfirmsMatch() throws Exception { + Block mockBlock = mock(Block.class); + ConfigurableBlockHook hook = new ConfigurableBlockHook("nexo:ruby_ore"); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + CustomBlockWrapper wrapper = customBlockWrapper("nexo:ruby_ore"); + + assertTrue(wrapper.equals(mockBlock)); + } + + @Test + @DisplayName("Given custom wrapper with no hooks, when comparing with block, then returns false") + void equalsBlock_returnsFalse_whenNoHooksRegistered() throws Exception { + Block mockBlock = mock(Block.class); + CustomBlockWrapper wrapper = customBlockWrapper("nexo:ruby_ore"); + + assertFalse(wrapper.equals(mockBlock)); + } + + @Test + @DisplayName("Given custom wrapper with hook that denies match, when comparing, then returns false") + void equalsBlock_returnsFalse_whenHookDeniesMatch() throws Exception { + Block mockBlock = mock(Block.class); + ConfigurableBlockHook hook = new ConfigurableBlockHook("nexo:sapphire_ore"); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + CustomBlockWrapper wrapper = customBlockWrapper("nexo:ruby_ore"); + + assertFalse(wrapper.equals(mockBlock)); + } + } + + @Nested + @DisplayName("itemBuilder") + class ItemBuilderTests { + + @BeforeEach + void setUpMockBukkit() { + MockBukkit.mock(); + CorePlugin mockPlugin = mock(CorePlugin.class); + when(mockPlugin.getMiniMessage()).thenReturn(MiniMessage.miniMessage()); + when(mockPlugin.getItemPlugin()).thenReturn(ItemPluginType.NONE); + corePluginStatic = mockStatic(CorePlugin.class); + corePluginStatic.when(CorePlugin::getInstance).thenReturn(mockPlugin); + } + + @AfterEach + void tearDownMockBukkit() { + corePluginStatic.close(); + MockBukkit.unmock(); + } + + private MockedStatic corePluginStatic; + + @Test + @DisplayName("Given vanilla material wrapper, when getting itemBuilder, then returns non-null builder") + void itemBuilder_returnsBuilder_whenVanillaMaterial() { + CustomBlockWrapper wrapper = materialWrapper(Material.STONE); + assertNotNull(wrapper.itemBuilder()); + } + + @Test + @DisplayName("Given custom block wrapper with no hooks, when getting itemBuilder, then returns AIR-based builder") + void itemBuilder_returnsAirBuilder_whenNoHooksRegistered() throws Exception { + CustomBlockWrapper wrapper = customBlockWrapper("nexo:ruby_ore"); + assertNotNull(wrapper.itemBuilder()); + } + + @Test + @DisplayName("Given custom block wrapper with hook providing item, when getting itemBuilder, then returns hook-provided builder") + void itemBuilder_returnsHookBuilder_whenHookProvidesItem() throws Exception { + ItemStack customItem = new ItemStack(Material.DIAMOND); + ConfigurableItemHookForBlock hook = new ConfigurableItemHookForBlock("nexo:ruby_ore", customItem); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + CustomBlockWrapper wrapper = customBlockWrapper("nexo:ruby_ore"); + + assertNotNull(wrapper.itemBuilder()); + } + } + + @Nested + @DisplayName("customModels (static)") + class CustomModelsTests { + + @Test + @DisplayName("Given no hooks registered, when getting customModels, then returns empty set in Optional") + void customModels_returnsEmptySet_whenNoHooksRegistered() { + Block mockBlock = mock(Block.class); + Optional> result = CustomBlockWrapper.customModels(mockBlock); + + assertTrue(result.isPresent()); + assertTrue(result.get().isEmpty()); + } + + @Test + @DisplayName("Given hook that returns models, when getting customModels, then returns those models") + void customModels_returnsModels_whenHookProvidesModels() { + Block mockBlock = mock(Block.class); + ConfigurableBlockHook hook = new ConfigurableBlockHook(false, Optional.of(Set.of("model_a", "model_b"))); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + Optional> result = CustomBlockWrapper.customModels(mockBlock); + + assertTrue(result.isPresent()); + assertEquals(Set.of("model_a", "model_b"), result.get()); + } + + @Test + @DisplayName("Given hook that returns empty optional, when getting customModels, then returns empty set") + void customModels_returnsEmptySet_whenHookReturnsEmptyOptional() { + Block mockBlock = mock(Block.class); + ConfigurableBlockHook hook = new ConfigurableBlockHook(false, Optional.empty()); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + Optional> result = CustomBlockWrapper.customModels(mockBlock); + + assertTrue(result.isPresent()); + assertTrue(result.get().isEmpty()); + } + } + + @Nested + @DisplayName("drops (static)") + class DropsTests { + + @Test + @DisplayName("Given hook registered, when getting drops, then returns hook-provided drops") + void drops_returnsHookDrops_whenHookRegistered() { + Block mockBlock = mock(Block.class); + ItemStack tool = mock(ItemStack.class); + Entity mockEntity = mock(Entity.class); + ItemStack expectedDrop = mock(ItemStack.class); + ConfigurableBlockHook hook = new ConfigurableBlockHook(List.of(expectedDrop)); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + List drops = CustomBlockWrapper.drops(mockBlock, tool, mockEntity); + + assertEquals(1, drops.size()); + assertEquals(expectedDrop, drops.get(0)); + } + + @Test + @DisplayName("Given no hooks registered, when getting drops, then returns block's vanilla drops") + void drops_returnsVanillaDrops_whenNoHooksRegistered() { + Block mockBlock = mock(Block.class); + ItemStack tool = mock(ItemStack.class); + Entity mockEntity = mock(Entity.class); + ItemStack vanillaDrop = mock(ItemStack.class); + when(mockBlock.getDrops(tool, mockEntity)).thenReturn(List.of(vanillaDrop)); + + List drops = CustomBlockWrapper.drops(mockBlock, tool, mockEntity); + + assertEquals(1, drops.size()); + assertEquals(vanillaDrop, drops.get(0)); + } + } + + @Nested + @DisplayName("removeBlock (static)") + class RemoveBlockTests { + + @Test + @DisplayName("Given hook registered, when removing block, then hook handles removal") + void removeBlock_delegatesToHook_whenHookRegistered() { + Block mockBlock = mock(Block.class); + TrackingBlockHook hook = new TrackingBlockHook(); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomBlockWrapper.removeBlock(mockBlock); + + assertTrue(hook.removeBlockCalled); + } + + @Test + @DisplayName("Given no hooks registered, when removing block, then sets block type to AIR") + void removeBlock_setsAir_whenNoHooksRegistered() { + Block mockBlock = mock(Block.class); + + CustomBlockWrapper.removeBlock(mockBlock); + + verify(mockBlock).setType(Material.AIR); + } + } + + @Nested + @DisplayName("playBlockDropEffects (static)") + class PlayBlockDropEffectsTests { + + @Test + @DisplayName("Given hook registered, when playing effects, then hook handles effects") + void playBlockDropEffects_delegatesToHook_whenHookRegistered() { + Block mockBlock = mock(Block.class); + TrackingBlockHook hook = new TrackingBlockHook(); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomBlockWrapper.playBlockDropEffects(mockBlock); + + assertTrue(hook.playEffectsCalled); + } + + @Test + @DisplayName("Given no hooks registered, when playing effects, then plays vanilla sound and particle") + void playBlockDropEffects_playsVanillaEffects_whenNoHooksRegistered() { + Block mockBlock = mock(Block.class); + World mockWorld = mock(World.class); + BlockData mockBlockData = mock(BlockData.class); + SoundGroup mockSoundGroup = mock(SoundGroup.class); + Location location = mock(Location.class); + Location clonedLocation = mock(Location.class); + Sound breakSound = Sound.BLOCK_STONE_BREAK; + + when(mockBlock.getWorld()).thenReturn(mockWorld); + when(mockBlock.getBlockData()).thenReturn(mockBlockData); + when(mockBlock.getBlockSoundGroup()).thenReturn(mockSoundGroup); + when(mockBlock.getLocation()).thenReturn(location); + when(location.clone()).thenReturn(clonedLocation); + when(clonedLocation.add(0.5, 0.5, 0.5)).thenReturn(clonedLocation); + when(mockSoundGroup.getBreakSound()).thenReturn(breakSound); + when(mockSoundGroup.getVolume()).thenReturn(1.0f); + when(mockSoundGroup.getPitch()).thenReturn(1.0f); + + CustomBlockWrapper.playBlockDropEffects(mockBlock); + + verify(mockWorld).playSound(eq(location), eq(breakSound), eq(1.0f), eq(1.0f)); + verify(mockWorld).spawnParticle( + eq(Particle.BLOCK), + eq(clonedLocation), + eq(20), + eq(0.25), + eq(0.25), + eq(0.25), + eq(0.05), + eq(mockBlockData)); + } } @Nested @@ -435,4 +813,183 @@ void toString_containsCustomBlockId_whenCustomBlockWrapper() throws Exception { assertTrue(result.contains("material=null"), "Expected material=null, got: " + result); } } + + static class ConfigurableBlockHook extends PluginHook implements CustomBlockHook { + + private final boolean isCustomBlock; + private final Optional> blockModels; + private final String matchingType; + private final List drops; + + ConfigurableBlockHook(boolean isCustomBlock, Optional> blockModels) { + super(null); + this.isCustomBlock = isCustomBlock; + this.blockModels = blockModels; + this.matchingType = null; + this.drops = List.of(); + } + + ConfigurableBlockHook(String matchingType) { + super(null); + this.isCustomBlock = false; + this.blockModels = Optional.empty(); + this.matchingType = matchingType; + this.drops = List.of(); + } + + ConfigurableBlockHook(List drops) { + super(null); + this.isCustomBlock = false; + this.blockModels = Optional.empty(); + this.matchingType = null; + this.drops = drops; + } + + @Override + public boolean isCustomBlock(@NotNull Block block) { + return isCustomBlock; + } + + @Override + public boolean isCustomBlock(@NotNull String customBlock) { + return false; + } + + @Override + public boolean isCustomBlockOfType(@NotNull Block block, @NotNull String customBlockType) { + return matchingType != null && matchingType.equals(customBlockType); + } + + @Override + public void placeCustomBlock(@NotNull Location location, @NotNull String blockId) { + } + + @NotNull + @Override + public List drops(@NotNull Block block, @NotNull ItemStack itemToBreakWith, @Nullable Entity entityBreaking) { + return drops; + } + + @Override + public void playBlockDropEffects(@NotNull Block block) { + } + + @Override + public void removeBlock(@NotNull Block block) { + } + + @NotNull + @Override + public Optional> blockModels(@NotNull Block block) { + return blockModels; + } + + @NotNull + @Override + public String blockName(@NotNull CustomBlockWrapper customBlockWrapper) { + return ""; + } + } + + static class TrackingBlockHook extends PluginHook implements CustomBlockHook { + + boolean removeBlockCalled; + boolean playEffectsCalled; + + TrackingBlockHook() { + super(null); + } + + @Override + public boolean isCustomBlock(@NotNull Block block) { + return false; + } + + @Override + public boolean isCustomBlock(@NotNull String customBlock) { + return false; + } + + @Override + public boolean isCustomBlockOfType(@NotNull Block block, @NotNull String customBlockType) { + return false; + } + + @Override + public void placeCustomBlock(@NotNull Location location, @NotNull String blockId) { + } + + @NotNull + @Override + public List drops(@NotNull Block block, @NotNull ItemStack itemToBreakWith, @Nullable Entity entityBreaking) { + return List.of(); + } + + @Override + public void playBlockDropEffects(@NotNull Block block) { + playEffectsCalled = true; + } + + @Override + public void removeBlock(@NotNull Block block) { + removeBlockCalled = true; + } + + @NotNull + @Override + public Optional> blockModels(@NotNull Block block) { + return Optional.empty(); + } + + @NotNull + @Override + public String blockName(@NotNull CustomBlockWrapper customBlockWrapper) { + return ""; + } + } + + static class ConfigurableItemHookForBlock extends PluginHook implements CustomItemHook { + + private final String itemName; + private final ItemStack item; + + ConfigurableItemHookForBlock(String itemName, ItemStack item) { + super(null); + this.itemName = itemName; + this.item = item; + } + + @Override + public boolean isItem(@NotNull String name) { + return itemName.equals(name); + } + + @Override + public boolean isItem(@NotNull ItemStack itemStack) { + return false; + } + + @Override + public boolean isItemOfType(@NotNull ItemStack itemStack, @NotNull String name) { + return false; + } + + @NotNull + @Override + public Optional item(@NotNull String name) { + return itemName.equals(name) ? Optional.of(item) : Optional.empty(); + } + + @NotNull + @Override + public Optional> itemModels(@NotNull ItemStack itemStack) { + return Optional.empty(); + } + + @NotNull + @Override + public String itemName(@NotNull CustomItemWrapper wrapper) { + return ""; + } + } } diff --git a/src/test/java/com/diamonddagger590/mccore/util/item/CustomEntityWrapperTest.java b/src/test/java/com/diamonddagger590/mccore/util/item/CustomEntityWrapperTest.java index f637b54..6f51529 100644 --- a/src/test/java/com/diamonddagger590/mccore/util/item/CustomEntityWrapperTest.java +++ b/src/test/java/com/diamonddagger590/mccore/util/item/CustomEntityWrapperTest.java @@ -14,6 +14,7 @@ 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.lang.reflect.Field; import java.util.Optional; @@ -24,6 +25,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class CustomEntityWrapperTest { @@ -391,5 +394,275 @@ void entityName_returnsHookProvidedName_whenHookIsRegistered() throws Exception CustomEntityWrapper wrapper = customEntityWrapper("mythicmobs:fire_dragon"); assertEquals("Fire Dragon", wrapper.entityName()); } + + @Test + @DisplayName("Given vanilla entity type wrapper, when getting entityName, then returns lang tag") + void entityName_returnsLangTag_whenVanillaEntityType() { + MockBukkit.mock(); + try { + CustomEntityWrapper wrapper = entityTypeWrapper(EntityType.ZOMBIE); + String name = wrapper.entityName(); + assertTrue(name.startsWith(""), "Expected lang tag to end with > but got: " + name); + } finally { + MockBukkit.unmock(); + } + } + + @Test + @DisplayName("Given custom entity with hook that doesn't recognize it, when getting entityName, then returns Unknown") + void entityName_returnsUnknown_whenHookDoesNotRecognize() throws Exception { + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(new TestCustomEntityPluginHook()); + CustomEntityWrapper wrapper = customEntityWrapper("mythicmobs:unknown_entity"); + assertEquals("Unknown", wrapper.entityName()); + } + } + + @Nested + @DisplayName("Constructor(Entity)") + class EntityConstructor { + + @Test + @DisplayName("Given an entity with no hooks registered, when constructing, then uses entity type") + void constructor_usesEntityType_whenNoHooksRegistered() { + Entity mockEntity = mock(Entity.class); + when(mockEntity.getType()).thenReturn(EntityType.ZOMBIE); + + CustomEntityWrapper wrapper = new CustomEntityWrapper(mockEntity); + + assertTrue(wrapper.isVanilla()); + assertFalse(wrapper.isCustom()); + assertTrue(wrapper.entityType().isPresent()); + assertEquals(EntityType.ZOMBIE, wrapper.entityType().get()); + } + + @Test + @DisplayName("Given an entity with hook that recognizes it and returns models, when constructing, then uses custom entity") + void constructor_usesCustomEntity_whenHookReturnsModels() { + Entity mockEntity = mock(Entity.class); + UUID uuid = UUID.randomUUID(); + when(mockEntity.getUniqueId()).thenReturn(uuid); + ConfigurableEntityHook hook = new ConfigurableEntityHook(true, Optional.of(Set.of("mythicmobs:fire_dragon"))); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomEntityWrapper wrapper = new CustomEntityWrapper(mockEntity); + + assertTrue(wrapper.isCustom()); + assertFalse(wrapper.isVanilla()); + assertTrue(wrapper.customEntity().isPresent()); + assertEquals("mythicmobs:fire_dragon", wrapper.customEntity().get()); + } + + @Test + @DisplayName("Given an entity with hook that recognizes it but returns empty models, when constructing, then uses entity type") + void constructor_usesEntityType_whenHookReturnsEmptyModels() { + Entity mockEntity = mock(Entity.class); + UUID uuid = UUID.randomUUID(); + when(mockEntity.getUniqueId()).thenReturn(uuid); + when(mockEntity.getType()).thenReturn(EntityType.CREEPER); + ConfigurableEntityHook hook = new ConfigurableEntityHook(true, Optional.of(Set.of())); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomEntityWrapper wrapper = new CustomEntityWrapper(mockEntity); + + assertTrue(wrapper.isVanilla()); + assertEquals(EntityType.CREEPER, wrapper.entityType().get()); + } + + @Test + @DisplayName("Given an entity with hook that does not recognize it, when constructing, then uses entity type") + void constructor_usesEntityType_whenHookDoesNotRecognize() { + Entity mockEntity = mock(Entity.class); + UUID uuid = UUID.randomUUID(); + when(mockEntity.getUniqueId()).thenReturn(uuid); + when(mockEntity.getType()).thenReturn(EntityType.SKELETON); + ConfigurableEntityHook hook = new ConfigurableEntityHook(false, Optional.empty()); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomEntityWrapper wrapper = new CustomEntityWrapper(mockEntity); + + assertTrue(wrapper.isVanilla()); + assertEquals(EntityType.SKELETON, wrapper.entityType().get()); + } + + @Test + @DisplayName("Given an entity with hook that returns empty optional for models, when constructing, then uses entity type") + void constructor_usesEntityType_whenHookReturnsEmptyOptional() { + Entity mockEntity = mock(Entity.class); + UUID uuid = UUID.randomUUID(); + when(mockEntity.getUniqueId()).thenReturn(uuid); + when(mockEntity.getType()).thenReturn(EntityType.SPIDER); + ConfigurableEntityHook hook = new ConfigurableEntityHook(true, Optional.empty()); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomEntityWrapper wrapper = new CustomEntityWrapper(mockEntity); + + assertTrue(wrapper.isVanilla()); + assertEquals(EntityType.SPIDER, wrapper.entityType().get()); + } + } + + @Nested + @DisplayName("equals(Entity)") + class EqualsEntity { + + @Test + @DisplayName("Given vanilla wrapper and entity with matching type, when comparing, then returns true") + void equalsEntity_returnsTrue_whenEntityTypeMatches() { + Entity mockEntity = mock(Entity.class); + when(mockEntity.getType()).thenReturn(EntityType.ZOMBIE); + CustomEntityWrapper wrapper = entityTypeWrapper(EntityType.ZOMBIE); + + assertTrue(wrapper.equals(mockEntity)); + } + + @Test + @DisplayName("Given vanilla wrapper and entity with different type, when comparing, then returns false") + void equalsEntity_returnsFalse_whenEntityTypeDiffers() { + Entity mockEntity = mock(Entity.class); + when(mockEntity.getType()).thenReturn(EntityType.SKELETON); + CustomEntityWrapper wrapper = entityTypeWrapper(EntityType.ZOMBIE); + + assertFalse(wrapper.equals(mockEntity)); + } + + @Test + @DisplayName("Given custom wrapper with hook that confirms type match, when comparing, then returns true") + void equalsEntity_returnsTrue_whenHookConfirmsMatch() throws Exception { + Entity mockEntity = mock(Entity.class); + UUID uuid = UUID.randomUUID(); + when(mockEntity.getUniqueId()).thenReturn(uuid); + ConfigurableEntityHook hook = new ConfigurableEntityHook(uuid, "mythicmobs:fire_dragon"); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + CustomEntityWrapper wrapper = customEntityWrapper("mythicmobs:fire_dragon"); + + assertTrue(wrapper.equals(mockEntity)); + } + + @Test + @DisplayName("Given custom wrapper with no hooks, when comparing with entity, then returns false") + void equalsEntity_returnsFalse_whenNoHooksRegistered() throws Exception { + Entity mockEntity = mock(Entity.class); + CustomEntityWrapper wrapper = customEntityWrapper("mythicmobs:fire_dragon"); + + assertFalse(wrapper.equals(mockEntity)); + } + + @Test + @DisplayName("Given custom wrapper with hook that denies match, when comparing, then returns false") + void equalsEntity_returnsFalse_whenHookDeniesMatch() throws Exception { + Entity mockEntity = mock(Entity.class); + UUID uuid = UUID.randomUUID(); + when(mockEntity.getUniqueId()).thenReturn(uuid); + ConfigurableEntityHook hook = new ConfigurableEntityHook(uuid, "mythicmobs:ice_dragon"); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + CustomEntityWrapper wrapper = customEntityWrapper("mythicmobs:fire_dragon"); + + assertFalse(wrapper.equals(mockEntity)); + } + } + + @Nested + @DisplayName("customModels (static)") + class CustomModelsTests { + + @Test + @DisplayName("Given no hooks registered, when getting customModels, then returns empty set in Optional") + void customModels_returnsEmptySet_whenNoHooksRegistered() { + Entity mockEntity = mock(Entity.class); + Optional> result = CustomEntityWrapper.customModels(mockEntity); + + assertTrue(result.isPresent()); + assertTrue(result.get().isEmpty()); + } + + @Test + @DisplayName("Given hook that returns models, when getting customModels, then returns those models") + void customModels_returnsModels_whenHookProvidesModels() { + Entity mockEntity = mock(Entity.class); + ConfigurableEntityHook hook = new ConfigurableEntityHook(false, Optional.of(Set.of("model_a", "model_b"))); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + Optional> result = CustomEntityWrapper.customModels(mockEntity); + + assertTrue(result.isPresent()); + assertEquals(Set.of("model_a", "model_b"), result.get()); + } + + @Test + @DisplayName("Given hook that returns empty optional, when getting customModels, then returns empty set") + void customModels_returnsEmptySet_whenHookReturnsEmptyOptional() { + Entity mockEntity = mock(Entity.class); + ConfigurableEntityHook hook = new ConfigurableEntityHook(false, Optional.empty()); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + Optional> result = CustomEntityWrapper.customModels(mockEntity); + + assertTrue(result.isPresent()); + assertTrue(result.get().isEmpty()); + } + } + + static class ConfigurableEntityHook extends PluginHook implements CustomEntityHook { + + private final boolean isCustomEntity; + private final Optional> entityModels; + private final UUID matchingUuid; + private final String matchingType; + + ConfigurableEntityHook(boolean isCustomEntity, Optional> entityModels) { + super(null); + this.isCustomEntity = isCustomEntity; + this.entityModels = entityModels; + this.matchingUuid = null; + this.matchingType = null; + } + + ConfigurableEntityHook(UUID matchingUuid, String matchingType) { + super(null); + this.isCustomEntity = false; + this.entityModels = Optional.empty(); + this.matchingUuid = matchingUuid; + this.matchingType = matchingType; + } + + @Override + public boolean isCustomEntity(@NotNull Entity entity) { + return isCustomEntity; + } + + @Override + public boolean isCustomEntity(@NotNull UUID uuid) { + return isCustomEntity || (matchingUuid != null && matchingUuid.equals(uuid)); + } + + @Override + public boolean isCustomEntity(@NotNull String customEntity) { + return false; + } + + @Override + public boolean isCustomEntityOfType(@NotNull Entity entity, @NotNull String customEntityType) { + return matchingType != null && matchingType.equals(customEntityType) + && matchingUuid != null && matchingUuid.equals(entity.getUniqueId()); + } + + @Override + public boolean isCustomEntityOfType(@NotNull UUID uuid, @NotNull String customEntityType) { + return matchingType != null && matchingType.equals(customEntityType) + && matchingUuid != null && matchingUuid.equals(uuid); + } + + @NotNull + @Override + public Optional> entityModels(@NotNull Entity entity) { + return entityModels; + } + + @NotNull + @Override + public String entityName(@NotNull CustomEntityWrapper customEntityWrapper) { + return ""; + } } } diff --git a/src/test/java/com/diamonddagger590/mccore/util/item/CustomItemWrapperTest.java b/src/test/java/com/diamonddagger590/mccore/util/item/CustomItemWrapperTest.java index 3d97bd0..cd9f00d 100644 --- a/src/test/java/com/diamonddagger590/mccore/util/item/CustomItemWrapperTest.java +++ b/src/test/java/com/diamonddagger590/mccore/util/item/CustomItemWrapperTest.java @@ -1,11 +1,13 @@ package com.diamonddagger590.mccore.util.item; import com.diamonddagger590.mccore.CorePlugin; +import com.diamonddagger590.mccore.builder.item.ItemPluginType; import com.diamonddagger590.mccore.external.common.CustomItemHook; import com.diamonddagger590.mccore.registry.RegistryAccess; import com.diamonddagger590.mccore.registry.RegistryKey; import com.diamonddagger590.mccore.registry.plugin.PluginHook; import com.diamonddagger590.mccore.testing.RegistryResetExtension; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -14,6 +16,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockito.MockedStatic; import java.lang.reflect.Field; import java.util.Optional; @@ -22,7 +26,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; class CustomItemWrapperTest { @@ -358,5 +366,264 @@ void itemName_returnsHookProvidedName_whenHookIsRegistered() throws Exception { CustomItemWrapper wrapper = customItemWrapper("nexo:ruby_sword"); assertEquals("Ruby Sword", wrapper.itemName()); } + + @Test + @DisplayName("Given vanilla material wrapper, when getting itemName, then returns lang tag") + void itemName_returnsLangTag_whenVanillaMaterial() { + MockBukkit.mock(); + try { + CustomItemWrapper wrapper = materialWrapper(Material.DIAMOND); + String name = wrapper.itemName(); + assertTrue(name.startsWith(""), "Expected lang tag to end with > but got: " + name); + } finally { + MockBukkit.unmock(); + } + } + + @Test + @DisplayName("Given custom item with hook that doesn't recognize it, when getting itemName, then returns Missing Item") + void itemName_returnsMissingItem_whenHookDoesNotRecognize() throws Exception { + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(new TestCustomItemPluginHook()); + CustomItemWrapper wrapper = customItemWrapper("nexo:unknown_item"); + assertEquals("Missing Item", wrapper.itemName()); + } + } + + @Nested + @DisplayName("Constructor(ItemStack)") + class ItemStackConstructor { + + @BeforeEach + void setUpMockBukkit() { + MockBukkit.mock(); + } + + @AfterEach + void tearDownMockBukkit() { + MockBukkit.unmock(); + } + + @Test + @DisplayName("Given an ItemStack with no hooks registered, when constructing, then uses item material") + void constructor_usesMaterial_whenNoHooksRegistered() { + ItemStack itemStack = new ItemStack(Material.DIAMOND_SWORD); + + CustomItemWrapper wrapper = new CustomItemWrapper(itemStack); + + assertTrue(wrapper.material().isPresent()); + assertEquals(Material.DIAMOND_SWORD, wrapper.material().get()); + assertFalse(wrapper.customItem().isPresent()); + } + + @Test + @DisplayName("Given an ItemStack with hook that recognizes it and returns models, when constructing, then uses custom item") + void constructor_usesCustomItem_whenHookReturnsModels() { + ItemStack itemStack = new ItemStack(Material.DIAMOND_SWORD); + ConfigurableItemHook hook = new ConfigurableItemHook(true, Optional.of(Set.of("nexo:ruby_sword"))); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomItemWrapper wrapper = new CustomItemWrapper(itemStack); + + assertTrue(wrapper.customItem().isPresent()); + assertEquals("nexo:ruby_sword", wrapper.customItem().get()); + assertFalse(wrapper.material().isPresent()); + } + + @Test + @DisplayName("Given an ItemStack with hook that recognizes it but returns empty models, when constructing, then uses material") + void constructor_usesMaterial_whenHookReturnsEmptyModels() { + ItemStack itemStack = new ItemStack(Material.IRON_INGOT); + ConfigurableItemHook hook = new ConfigurableItemHook(true, Optional.of(Set.of())); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomItemWrapper wrapper = new CustomItemWrapper(itemStack); + + assertTrue(wrapper.material().isPresent()); + assertEquals(Material.IRON_INGOT, wrapper.material().get()); + } + + @Test + @DisplayName("Given an ItemStack with hook that does not recognize it, when constructing, then uses material") + void constructor_usesMaterial_whenHookDoesNotRecognize() { + ItemStack itemStack = new ItemStack(Material.STONE); + ConfigurableItemHook hook = new ConfigurableItemHook(false, Optional.empty()); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomItemWrapper wrapper = new CustomItemWrapper(itemStack); + + assertTrue(wrapper.material().isPresent()); + assertEquals(Material.STONE, wrapper.material().get()); + } + + @Test + @DisplayName("Given an ItemStack with hook that returns empty optional for models, when constructing, then uses material") + void constructor_usesMaterial_whenHookReturnsEmptyOptional() { + ItemStack itemStack = new ItemStack(Material.GOLD_INGOT); + ConfigurableItemHook hook = new ConfigurableItemHook(true, Optional.empty()); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + CustomItemWrapper wrapper = new CustomItemWrapper(itemStack); + + assertTrue(wrapper.material().isPresent()); + assertEquals(Material.GOLD_INGOT, wrapper.material().get()); + } + } + + @Nested + @DisplayName("itemBuilder") + class ItemBuilderTests { + + private MockedStatic corePluginStatic; + + @BeforeEach + void setUpMockBukkit() { + MockBukkit.mock(); + CorePlugin mockPlugin = mock(CorePlugin.class); + when(mockPlugin.getMiniMessage()).thenReturn(MiniMessage.miniMessage()); + when(mockPlugin.getItemPlugin()).thenReturn(ItemPluginType.NONE); + when(mockPlugin.registryAccess()).thenCallRealMethod(); + corePluginStatic = mockStatic(CorePlugin.class); + corePluginStatic.when(CorePlugin::getInstance).thenReturn(mockPlugin); + } + + @AfterEach + void tearDownMockBukkit() { + corePluginStatic.close(); + MockBukkit.unmock(); + } + + @Test + @DisplayName("Given vanilla material wrapper, when getting itemBuilder, then returns non-null builder") + void itemBuilder_returnsBuilder_whenVanillaMaterial() { + CustomItemWrapper wrapper = materialWrapper(Material.STONE); + assertNotNull(wrapper.itemBuilder()); + } + + @Test + @DisplayName("Given custom item wrapper with no hooks, when getting itemBuilder, then returns AIR-based builder") + void itemBuilder_returnsAirBuilder_whenNoHooksRegistered() throws Exception { + CustomItemWrapper wrapper = customItemWrapper("nexo:ruby_sword"); + assertNotNull(wrapper.itemBuilder()); + } + + @Test + @DisplayName("Given custom item wrapper with hook providing item, when getting itemBuilder, then returns hook-provided builder") + void itemBuilder_returnsHookBuilder_whenHookProvidesItem() throws Exception { + ItemStack customItem = new ItemStack(Material.DIAMOND); + ConfigurableItemHook hook = new ConfigurableItemHook("nexo:ruby_sword", customItem); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + CustomItemWrapper wrapper = customItemWrapper("nexo:ruby_sword"); + + assertNotNull(wrapper.itemBuilder()); + } + } + + @Nested + @DisplayName("customModels (static)") + class CustomModelsTests { + + @BeforeEach + void setUpMockBukkit() { + MockBukkit.mock(); + } + + @AfterEach + void tearDownMockBukkit() { + MockBukkit.unmock(); + } + + @Test + @DisplayName("Given no hooks registered, when getting customModels, then returns empty set in Optional") + void customModels_returnsEmptySet_whenNoHooksRegistered() { + ItemStack itemStack = new ItemStack(Material.DIAMOND_SWORD); + Optional> result = CustomItemWrapper.customModels(itemStack); + + assertTrue(result.isPresent()); + assertTrue(result.get().isEmpty()); + } + + @Test + @DisplayName("Given hook that returns models, when getting customModels, then returns those models") + void customModels_returnsModels_whenHookProvidesModels() { + ItemStack itemStack = new ItemStack(Material.DIAMOND_SWORD); + ConfigurableItemHook hook = new ConfigurableItemHook(false, Optional.of(Set.of("model_a", "model_b"))); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + Optional> result = CustomItemWrapper.customModels(itemStack); + + assertTrue(result.isPresent()); + assertEquals(Set.of("model_a", "model_b"), result.get()); + } + + @Test + @DisplayName("Given hook that returns empty optional, when getting customModels, then returns empty set") + void customModels_returnsEmptySet_whenHookReturnsEmptyOptional() { + ItemStack itemStack = new ItemStack(Material.DIAMOND_SWORD); + ConfigurableItemHook hook = new ConfigurableItemHook(false, Optional.empty()); + RegistryAccess.registryAccess().registry(RegistryKey.PLUGIN_HOOK).register(hook); + + Optional> result = CustomItemWrapper.customModels(itemStack); + + assertTrue(result.isPresent()); + assertTrue(result.get().isEmpty()); + } + } + + static class ConfigurableItemHook extends PluginHook implements CustomItemHook { + + private final boolean isItem; + private final Optional> models; + private final String itemForName; + private final ItemStack providedItem; + + ConfigurableItemHook(boolean isItem, Optional> models) { + super(null); + this.isItem = isItem; + this.models = models; + this.itemForName = null; + this.providedItem = null; + } + + ConfigurableItemHook(String itemForName, ItemStack providedItem) { + super(null); + this.isItem = false; + this.models = Optional.empty(); + this.itemForName = itemForName; + this.providedItem = providedItem; + } + + @Override + public boolean isItem(@NotNull String itemName) { + return false; + } + + @Override + public boolean isItem(@NotNull ItemStack itemStack) { + return isItem; + } + + @Override + public boolean isItemOfType(@NotNull ItemStack itemStack, @NotNull String itemName) { + return false; + } + + @NotNull + @Override + public Optional item(@NotNull String itemName) { + return itemForName != null && itemForName.equals(itemName) ? Optional.ofNullable(providedItem) : Optional.empty(); + } + + @NotNull + @Override + public Optional> itemModels(@NotNull ItemStack itemStack) { + return models; + } + + @NotNull + @Override + public String itemName(@NotNull CustomItemWrapper customItemWrapper) { + return ""; + } } }