From 2e96f7bf7e5c91e109131edfaeead6c24306d4cf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 09:41:45 +0000 Subject: [PATCH 1/2] Add unit tests for CoreItemsAdderHook, CoreCMIHook, and CoreCitizensHook Add comprehensive test coverage for three external plugin hook classes that previously had minimal or no test coverage: - CoreItemsAdderHookTest: 46 tests covering item/block operations, itemName/blockName with all fallback paths (Adventure component, legacy display name, formatted ID, material name), case-insensitive matching, drops, effects, and block removal. - CoreCMIHookTest: 7 tests covering isAfk (online+AFK, online+notAFK, null CMIUser, offline player) and isEntityNpc (Player NPC, Player not NPC, non-Player entity). - CoreCitizensHookTest: 3 tests covering isEntityNpc with Entity, Player as NPC, and non-NPC entity. Also adds testImplementation dependencies for ItemsAdder, CMI-API, and Citizens to make their classes available on the test classpath, following the existing pattern used for paper-api and placeholderapi. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_018B4s5X96MdWqpXpW83hex1 --- build.gradle.kts | 5 + .../citizens/CoreCitizensHookTest.java | 76 ++ .../mccore/external/cmi/CoreCMIHookTest.java | 156 ++++ .../itemsadder/CoreItemsAdderHookTest.java | 821 ++++++++++++++++++ 4 files changed, 1058 insertions(+) create mode 100644 src/test/java/com/diamonddagger590/mccore/external/citizens/CoreCitizensHookTest.java create mode 100644 src/test/java/com/diamonddagger590/mccore/external/cmi/CoreCMIHookTest.java create mode 100644 src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java diff --git a/build.gradle.kts b/build.gradle.kts index d548155..fbb4853 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -96,6 +96,7 @@ dependencies { // Custom items val itemsAdderVersion = "4.0.10" compileOnly("dev.lone:api-itemsadder:$itemsAdderVersion") + testImplementation("dev.lone:api-itemsadder:$itemsAdderVersion") val nexoVersion = "1.16.0" compileOnly("com.nexomc:nexo:$nexoVersion") { exclude(group = "net.byteflux") @@ -130,11 +131,15 @@ dependencies { val cmiVersion = "9.7.14.3" compileOnly("com.github.Zrips:CMI-API:$cmiVersion") + testImplementation("com.github.Zrips:CMI-API:$cmiVersion") val citizensVersion = "2.0.39-SNAPSHOT" compileOnly("net.citizensnpcs:citizens-main:$citizensVersion") { exclude(group = "*", module = "*") } + testImplementation("net.citizensnpcs:citizens-main:$citizensVersion") { + exclude(group = "*", module = "*") + } } diff --git a/src/test/java/com/diamonddagger590/mccore/external/citizens/CoreCitizensHookTest.java b/src/test/java/com/diamonddagger590/mccore/external/citizens/CoreCitizensHookTest.java new file mode 100644 index 0000000..ceec4c9 --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/external/citizens/CoreCitizensHookTest.java @@ -0,0 +1,76 @@ +package com.diamonddagger590.mccore.external.citizens; + +import com.diamonddagger590.mccore.CorePlugin; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.npc.NPCRegistry; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertFalse; +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; + +@ExtendWith(MockitoExtension.class) +class CoreCitizensHookTest { + + @Mock + private CorePlugin plugin; + + private CoreCitizensHook hook; + + @BeforeEach + void setUp() { + hook = new CoreCitizensHook(plugin); + } + + @Test + @DisplayName("Returns true when entity is an NPC according to Citizens") + void isEntityNpc_returnsTrue_whenCitizensReportsNpc() { + Entity entity = mock(Entity.class); + + try (MockedStatic mocked = mockStatic(CitizensAPI.class)) { + NPCRegistry npcRegistry = mock(NPCRegistry.class); + mocked.when(CitizensAPI::getNPCRegistry).thenReturn(npcRegistry); + when(npcRegistry.isNPC(entity)).thenReturn(true); + + assertTrue(hook.isEntityNpc(entity)); + } + } + + @Test + @DisplayName("Returns false when entity is not an NPC according to Citizens") + void isEntityNpc_returnsFalse_whenCitizensReportsNotNpc() { + Entity entity = mock(Entity.class); + + try (MockedStatic mocked = mockStatic(CitizensAPI.class)) { + NPCRegistry npcRegistry = mock(NPCRegistry.class); + mocked.when(CitizensAPI::getNPCRegistry).thenReturn(npcRegistry); + when(npcRegistry.isNPC(entity)).thenReturn(false); + + assertFalse(hook.isEntityNpc(entity)); + } + } + + @Test + @DisplayName("Returns true when entity is a Player and Citizens reports NPC") + void isEntityNpc_returnsTrue_whenPlayerIsNpc() { + Player player = mock(Player.class); + + try (MockedStatic mocked = mockStatic(CitizensAPI.class)) { + NPCRegistry npcRegistry = mock(NPCRegistry.class); + mocked.when(CitizensAPI::getNPCRegistry).thenReturn(npcRegistry); + when(npcRegistry.isNPC(player)).thenReturn(true); + + assertTrue(hook.isEntityNpc(player)); + } + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/external/cmi/CoreCMIHookTest.java b/src/test/java/com/diamonddagger590/mccore/external/cmi/CoreCMIHookTest.java new file mode 100644 index 0000000..f400001 --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/external/cmi/CoreCMIHookTest.java @@ -0,0 +1,156 @@ +package com.diamonddagger590.mccore.external.cmi; + +import com.Zrips.CMI.CMI; +import com.Zrips.CMI.Containers.CMIUser; +import com.Zrips.CMI.PlayerManager; +import com.Zrips.CMI.utils.Util; +import com.diamonddagger590.mccore.CorePlugin; +import com.diamonddagger590.mccore.player.CorePlayer; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +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.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertFalse; +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; + +@ExtendWith(MockitoExtension.class) +class CoreCMIHookTest { + + @Mock + private CorePlugin plugin; + + private CoreCMIHook hook; + + @BeforeEach + void setUp() { + hook = new CoreCMIHook(plugin); + } + + @Nested + @DisplayName("isAfk") + class IsAfkTests { + + @Mock + private CorePlayer corePlayer; + + @Test + @DisplayName("Returns true when player is online and CMI reports AFK") + void isAfk_returnsTrue_whenPlayerOnlineAndAfk() { + Player player = mock(Player.class); + when(corePlayer.getAsBukkitPlayer()).thenReturn(Optional.of(player)); + + try (MockedStatic mocked = mockStatic(CMI.class)) { + CMI cmi = mock(CMI.class); + mocked.when(CMI::getInstance).thenReturn(cmi); + PlayerManager playerManager = mock(PlayerManager.class); + when(cmi.getPlayerManager()).thenReturn(playerManager); + CMIUser cmiUser = mock(CMIUser.class); + when(playerManager.getUser(player)).thenReturn(cmiUser); + when(cmiUser.isAfk()).thenReturn(true); + + assertTrue(hook.isAfk(corePlayer)); + } + } + + @Test + @DisplayName("Returns false when player is online and CMI reports not AFK") + void isAfk_returnsFalse_whenPlayerOnlineAndNotAfk() { + Player player = mock(Player.class); + when(corePlayer.getAsBukkitPlayer()).thenReturn(Optional.of(player)); + + try (MockedStatic mocked = mockStatic(CMI.class)) { + CMI cmi = mock(CMI.class); + mocked.when(CMI::getInstance).thenReturn(cmi); + PlayerManager playerManager = mock(PlayerManager.class); + when(cmi.getPlayerManager()).thenReturn(playerManager); + CMIUser cmiUser = mock(CMIUser.class); + when(playerManager.getUser(player)).thenReturn(cmiUser); + when(cmiUser.isAfk()).thenReturn(false); + + assertFalse(hook.isAfk(corePlayer)); + } + } + + @Test + @DisplayName("Returns false when CMIUser is null") + void isAfk_returnsFalse_whenCmiUserIsNull() { + Player player = mock(Player.class); + when(corePlayer.getAsBukkitPlayer()).thenReturn(Optional.of(player)); + + try (MockedStatic mocked = mockStatic(CMI.class)) { + CMI cmi = mock(CMI.class); + mocked.when(CMI::getInstance).thenReturn(cmi); + PlayerManager playerManager = mock(PlayerManager.class); + when(cmi.getPlayerManager()).thenReturn(playerManager); + when(playerManager.getUser(player)).thenReturn(null); + + assertFalse(hook.isAfk(corePlayer)); + } + } + + @Test + @DisplayName("Returns false when player is offline") + void isAfk_returnsFalse_whenPlayerOffline() { + when(corePlayer.getAsBukkitPlayer()).thenReturn(Optional.empty()); + + assertFalse(hook.isAfk(corePlayer)); + } + } + + @Nested + @DisplayName("isEntityNpc") + class IsEntityNpcTests { + + @Test + @DisplayName("Returns true when entity is a Player and CMI reports NPC") + void isEntityNpc_returnsTrue_whenPlayerIsNpc() { + Player player = mock(Player.class); + + try (MockedStatic mocked = mockStatic(CMI.class)) { + CMI cmi = mock(CMI.class); + mocked.when(CMI::getInstance).thenReturn(cmi); + Util utilManager = mock(Util.class); + when(cmi.getUtilManager()).thenReturn(utilManager); + when(utilManager.isNPC(player)).thenReturn(true); + + assertTrue(hook.isEntityNpc(player)); + } + } + + @Test + @DisplayName("Returns false when entity is a Player but CMI says not NPC") + void isEntityNpc_returnsFalse_whenPlayerIsNotNpc() { + Player player = mock(Player.class); + + try (MockedStatic mocked = mockStatic(CMI.class)) { + CMI cmi = mock(CMI.class); + mocked.when(CMI::getInstance).thenReturn(cmi); + Util utilManager = mock(Util.class); + when(cmi.getUtilManager()).thenReturn(utilManager); + when(utilManager.isNPC(player)).thenReturn(false); + + assertFalse(hook.isEntityNpc(player)); + } + } + + @Test + @DisplayName("Returns false when entity is not a Player") + void isEntityNpc_returnsFalse_whenEntityIsNotPlayer() { + Entity entity = mock(Entity.class); + + assertFalse(hook.isEntityNpc(entity)); + } + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java b/src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java new file mode 100644 index 0000000..437ce23 --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java @@ -0,0 +1,821 @@ +package com.diamonddagger590.mccore.external.itemsadder; + +import com.diamonddagger590.mccore.CorePlugin; +import com.diamonddagger590.mccore.util.item.CustomBlockWrapper; +import com.diamonddagger590.mccore.util.item.CustomItemWrapper; +import dev.lone.itemsadder.api.CustomBlock; +import dev.lone.itemsadder.api.CustomStack; +import net.kyori.adventure.text.Component; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +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; +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.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +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; + +@ExtendWith(MockitoExtension.class) +class CoreItemsAdderHookTest { + + @Mock + private CorePlugin plugin; + + private CoreItemsAdderHook hook; + + @BeforeEach + void setUp() { + hook = new CoreItemsAdderHook(plugin); + } + + @Nested + @DisplayName("item(String)") + class ItemTests { + + @Test + @DisplayName("Returns the ItemStack when CustomStack exists for the given item ID") + void item_returnsItemStack_whenCustomStackExists() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + ItemStack expectedItem = mock(ItemStack.class); + mocked.when(() -> CustomStack.getInstance("my_item")).thenReturn(customStack); + when(customStack.getItemStack()).thenReturn(expectedItem); + + Optional result = hook.item("my_item"); + + assertTrue(result.isPresent()); + assertEquals(expectedItem, result.get()); + } + } + + @Test + @DisplayName("Returns empty when CustomStack does not exist for the given item ID") + void item_returnsEmpty_whenCustomStackDoesNotExist() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + mocked.when(() -> CustomStack.getInstance("nonexistent")).thenReturn(null); + + Optional result = hook.item("nonexistent"); + + assertFalse(result.isPresent()); + } + } + } + + @Nested + @DisplayName("itemModels(ItemStack)") + class ItemModelsTests { + + @Test + @DisplayName("Returns model path set when CustomStack exists for the item") + void itemModels_returnsModelSet_whenCustomStackExists() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + ItemStack itemStack = mock(ItemStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(customStack); + when(customStack.getModelPath()).thenReturn("my_namespace:my_model"); + + Optional> result = hook.itemModels(itemStack); + + assertTrue(result.isPresent()); + assertEquals(Set.of("my_namespace:my_model"), result.get()); + } + } + + @Test + @DisplayName("Returns empty when CustomStack does not exist for the item") + void itemModels_returnsEmpty_whenNoCustomStack() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(null); + + Optional> result = hook.itemModels(itemStack); + + assertFalse(result.isPresent()); + } + } + } + + @Nested + @DisplayName("isItem") + class IsItemTests { + + @Test + @DisplayName("isItem(String) returns true when item is in registry") + void isItem_string_returnsTrue_whenInRegistry() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + mocked.when(() -> CustomStack.isInRegistry("valid_item")).thenReturn(true); + + assertTrue(hook.isItem("valid_item")); + } + } + + @Test + @DisplayName("isItem(String) returns false when item is not in registry") + void isItem_string_returnsFalse_whenNotInRegistry() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + mocked.when(() -> CustomStack.isInRegistry("invalid_item")).thenReturn(false); + + assertFalse(hook.isItem("invalid_item")); + } + } + + @Test + @DisplayName("isItem(ItemStack) returns true when CustomStack exists") + void isItem_itemStack_returnsTrue_whenCustomStackExists() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(mock(CustomStack.class)); + + assertTrue(hook.isItem(itemStack)); + } + } + + @Test + @DisplayName("isItem(ItemStack) returns false when CustomStack does not exist") + void isItem_itemStack_returnsFalse_whenNoCustomStack() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(null); + + assertFalse(hook.isItem(itemStack)); + } + } + } + + @Nested + @DisplayName("isItemOfType") + class IsItemOfTypeTests { + + @Test + @DisplayName("Returns true when item matches the given type name") + void isItemOfType_returnsTrue_whenMatches() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(customStack); + when(customStack.getModelPath()).thenReturn("my_sword"); + + assertTrue(hook.isItemOfType(itemStack, "my_sword")); + } + } + + @Test + @DisplayName("Returns true when item matches type name case-insensitively") + void isItemOfType_returnsTrue_caseInsensitive() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(customStack); + when(customStack.getModelPath()).thenReturn("My_Sword"); + + assertTrue(hook.isItemOfType(itemStack, "my_sword")); + } + } + + @Test + @DisplayName("Returns false when no CustomStack exists for the item") + void isItemOfType_returnsFalse_whenNoCustomStack() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(null); + + assertFalse(hook.isItemOfType(itemStack, "my_sword")); + } + } + + @Test + @DisplayName("Returns false when item type does not match") + void isItemOfType_returnsFalse_whenTypeMismatch() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(customStack); + when(customStack.getModelPath()).thenReturn("other_item"); + + assertFalse(hook.isItemOfType(itemStack, "my_sword")); + } + } + } + + @Nested + @DisplayName("Block operations") + class BlockTests { + + @Mock + private Block block; + + @Test + @DisplayName("isCustomBlock(Block) returns true when block is a custom block") + void isCustomBlock_returnsTrue_whenCustomBlockExists() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(mock(CustomBlock.class)); + + assertTrue(hook.isCustomBlock(block)); + } + } + + @Test + @DisplayName("isCustomBlock(Block) returns false when block is not a custom block") + void isCustomBlock_returnsFalse_whenNotCustomBlock() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + + assertFalse(hook.isCustomBlock(block)); + } + } + + @Test + @DisplayName("isCustomBlock(String) always returns false for ItemsAdder") + void isCustomBlock_string_alwaysReturnsFalse() { + assertFalse(hook.isCustomBlock("any_block")); + } + + @Test + @DisplayName("isCustomBlockOfType returns true when block matches the type") + void isCustomBlockOfType_returnsTrue_whenMatches() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + when(customBlock.getModelPath()).thenReturn("my_block"); + + assertTrue(hook.isCustomBlockOfType(block, "my_block")); + } + } + + @Test + @DisplayName("isCustomBlockOfType returns true case-insensitively") + void isCustomBlockOfType_returnsTrue_caseInsensitive() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + when(customBlock.getModelPath()).thenReturn("My_Block"); + + assertTrue(hook.isCustomBlockOfType(block, "my_block")); + } + } + + @Test + @DisplayName("isCustomBlockOfType returns false when block is not custom") + void isCustomBlockOfType_returnsFalse_whenNotCustom() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + + assertFalse(hook.isCustomBlockOfType(block, "my_block")); + } + } + + @Test + @DisplayName("isCustomBlockOfType returns false when type does not match") + void isCustomBlockOfType_returnsFalse_whenTypeMismatch() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + when(customBlock.getModelPath()).thenReturn("other_block"); + + assertFalse(hook.isCustomBlockOfType(block, "my_block")); + } + } + + @Test + @DisplayName("blockModels returns model set when custom block exists") + void blockModels_returnsModelSet_whenCustomBlockExists() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + when(customBlock.getModelPath()).thenReturn("ns:my_model"); + + Optional> result = hook.blockModels(block); + + assertTrue(result.isPresent()); + assertEquals(Set.of("ns:my_model"), result.get()); + } + } + + @Test + @DisplayName("blockModels returns empty when block is not custom") + void blockModels_returnsEmpty_whenNotCustom() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + + assertFalse(hook.blockModels(block).isPresent()); + } + } + } + + @Nested + @DisplayName("placeCustomBlock") + class PlaceCustomBlockTests { + + @Test + @DisplayName("Throws when blockId is not a valid custom block") + void placeCustomBlock_throwsIllegalArgument_whenNotValidBlock() { + Location location = mock(Location.class); + + assertThrows(IllegalArgumentException.class, + () -> hook.placeCustomBlock(location, "invalid_block")); + } + } + + @Nested + @DisplayName("drops") + class DropsTests { + + @Mock + private Block block; + + @Test + @DisplayName("Returns custom block drops when block is a custom block") + void drops_returnsCustomDrops_whenCustomBlock() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + ItemStack tool = mock(ItemStack.class); + Entity entity = mock(Entity.class); + List expectedDrops = List.of(mock(ItemStack.class)); + + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(mock(CustomBlock.class)); + mocked.when(() -> CustomBlock.getLoot(block, tool, true)).thenReturn(expectedDrops); + + List result = hook.drops(block, tool, entity); + + assertEquals(expectedDrops, result); + } + } + + @SuppressWarnings("unchecked") + @Test + @DisplayName("Returns vanilla block drops when block is not custom") + void drops_returnsVanillaDrops_whenNotCustomBlock() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + ItemStack tool = mock(ItemStack.class); + Entity entity = mock(Entity.class); + ItemStack dropItem = mock(ItemStack.class); + Collection drops = List.of(dropItem); + + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + when(block.getDrops(tool, entity)).thenReturn(drops); + + List result = hook.drops(block, tool, entity); + + assertEquals(1, result.size()); + assertTrue(result.contains(dropItem)); + } + } + + @SuppressWarnings("unchecked") + @Test + @DisplayName("Returns vanilla block drops when entity is null") + void drops_returnsVanillaDrops_whenEntityIsNull() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + ItemStack tool = mock(ItemStack.class); + ItemStack dropItem = mock(ItemStack.class); + Collection drops = List.of(dropItem); + + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + when(block.getDrops(tool, null)).thenReturn(drops); + + List result = hook.drops(block, tool, null); + + assertEquals(1, result.size()); + assertTrue(result.contains(dropItem)); + } + } + } + + @Nested + @DisplayName("playBlockDropEffects") + class PlayBlockDropEffectsTests { + + @Mock + private Block block; + + @Test + @DisplayName("Plays custom block effects when block is custom") + void playBlockDropEffects_playsCustomEffects_whenCustomBlock() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + + hook.playBlockDropEffects(block); + + verify(customBlock).playBreakEffect(); + verify(customBlock).playBreakSound(); + verify(customBlock).playBreakParticles(); + } + } + + @Test + @DisplayName("Plays vanilla block effects when block is not custom") + void playBlockDropEffects_playsVanillaEffects_whenNotCustomBlock() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + + World world = mock(World.class); + BlockData blockData = mock(BlockData.class); + SoundGroup soundGroup = mock(SoundGroup.class); + Location location = mock(Location.class); + + when(block.getWorld()).thenReturn(world); + when(block.getBlockData()).thenReturn(blockData); + when(block.getBlockSoundGroup()).thenReturn(soundGroup); + when(block.getLocation()).thenReturn(location); + when(location.clone()).thenReturn(location); + when(location.add(0.5, 0.5, 0.5)).thenReturn(location); + when(soundGroup.getBreakSound()).thenReturn(org.bukkit.Sound.BLOCK_STONE_BREAK); + when(soundGroup.getVolume()).thenReturn(1.0f); + when(soundGroup.getPitch()).thenReturn(1.0f); + + hook.playBlockDropEffects(block); + + verify(world).playSound(eq(location), eq(org.bukkit.Sound.BLOCK_STONE_BREAK), eq(1.0f), eq(1.0f)); + verify(world).spawnParticle(eq(Particle.BLOCK), eq(location), eq(20), + eq(0.25), eq(0.25), eq(0.25), eq(0.05), eq(blockData)); + } + } + } + + @Nested + @DisplayName("removeBlock") + class RemoveBlockTests { + + @Mock + private Block block; + + @Test + @DisplayName("Removes custom block when it is a custom block and removal succeeds") + void removeBlock_removesCustomBlock_whenSuccessful() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + when(customBlock.remove()).thenReturn(true); + + hook.removeBlock(block); + + verify(customBlock).remove(); + } + } + + @Test + @DisplayName("Throws when custom block removal fails") + void removeBlock_throwsIllegalState_whenRemovalFails() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + when(customBlock.remove()).thenReturn(false); + when(customBlock.getModelPath()).thenReturn("ns:failing_block"); + Location loc = mock(Location.class); + when(block.getLocation()).thenReturn(loc); + + assertThrows(IllegalStateException.class, () -> hook.removeBlock(block)); + } + } + + @Test + @DisplayName("Sets block to AIR when it is not a custom block") + void removeBlock_setsAir_whenNotCustomBlock() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + + hook.removeBlock(block); + + verify(block).setType(Material.AIR); + } + } + } + + @Nested + @DisplayName("itemName") + class ItemNameTests { + + @Test + @DisplayName("Returns Adventure component name when available") + void itemName_returnsComponentName_whenAvailable() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:my_item")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(Component.text("Fancy Sword")); + + CustomItemWrapper wrapper = new TestCustomItemWrapper("ns:my_item"); + String result = hook.itemName(wrapper); + + assertEquals("Fancy Sword", result); + } + } + + @Test + @DisplayName("Falls back to legacy display name when component name is null") + void itemName_returnsLegacyName_whenComponentNull() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:my_item")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(null); + when(customStack.getDisplayName()).thenReturn("§6Golden Blade"); + + CustomItemWrapper wrapper = new TestCustomItemWrapper("ns:my_item"); + String result = hook.itemName(wrapper); + + assertEquals("Golden Blade", result); + } + } + + @Test + @DisplayName("Falls back to legacy display name when component name is empty") + void itemName_returnsLegacyName_whenComponentEmpty() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:my_item")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(Component.empty()); + when(customStack.getDisplayName()).thenReturn("Legacy Name"); + + CustomItemWrapper wrapper = new TestCustomItemWrapper("ns:my_item"); + String result = hook.itemName(wrapper); + + assertEquals("Legacy Name", result); + } + } + + @Test + @DisplayName("Falls back to formatted block ID when both names are unavailable") + void itemName_returnsFormattedId_whenBothNamesUnavailable() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:cool_item")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(null); + when(customStack.getDisplayName()).thenReturn(null); + + CustomItemWrapper wrapper = new TestCustomItemWrapper("ns:cool_item"); + String result = hook.itemName(wrapper); + + assertEquals("Cool Item", result); + } + } + + @Test + @DisplayName("Falls back to formatted block ID when legacy name is empty") + void itemName_returnsFormattedId_whenLegacyNameEmpty() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:cool_item")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(null); + when(customStack.getDisplayName()).thenReturn(""); + + CustomItemWrapper wrapper = new TestCustomItemWrapper("ns:cool_item"); + String result = hook.itemName(wrapper); + + assertEquals("Cool Item", result); + } + } + + @Test + @DisplayName("Falls back to formatted block ID when CustomStack is null") + void itemName_returnsFormattedId_whenCustomStackNull() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + mocked.when(() -> CustomStack.getInstance("ns:cool_item")).thenReturn(null); + + CustomItemWrapper wrapper = new TestCustomItemWrapper("ns:cool_item"); + String result = hook.itemName(wrapper); + + assertEquals("Cool Item", result); + } + } + + @Test + @DisplayName("Returns formatted material name when wrapper has no custom item") + void itemName_returnsFormattedMaterial_whenVanilla() { + CustomItemWrapper wrapper = new TestVanillaItemWrapper(Material.OAK_LOG); + String result = hook.itemName(wrapper); + + assertEquals("Oak Log", result); + } + } + + @Nested + @DisplayName("blockName") + class BlockNameTests { + + @Test + @DisplayName("Returns Adventure component name when available") + void blockName_returnsComponentName_whenAvailable() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:my_block")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(Component.text("Fancy Block")); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("ns:my_block"); + String result = hook.blockName(wrapper); + + assertEquals("Fancy Block", result); + } + } + + @Test + @DisplayName("Falls back to legacy display name when component name is null") + void blockName_returnsLegacyName_whenComponentNull() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:my_block")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(null); + when(customStack.getDisplayName()).thenReturn("§aGreen Block"); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("ns:my_block"); + String result = hook.blockName(wrapper); + + assertEquals("Green Block", result); + } + } + + @Test + @DisplayName("Falls back to legacy display name when component name is empty") + void blockName_returnsLegacyName_whenComponentEmpty() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:my_block")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(Component.empty()); + when(customStack.getDisplayName()).thenReturn("Legacy Block"); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("ns:my_block"); + String result = hook.blockName(wrapper); + + assertEquals("Legacy Block", result); + } + } + + @Test + @DisplayName("Falls back to formatted block ID when legacy name is empty") + void blockName_returnsFormattedId_whenLegacyNameEmpty() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:cool_block")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(null); + when(customStack.getDisplayName()).thenReturn(""); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("ns:cool_block"); + String result = hook.blockName(wrapper); + + assertEquals("Cool Block", result); + } + } + + @Test + @DisplayName("Falls back to formatted block ID when both names are unavailable") + void blockName_returnsFormattedId_whenBothNamesUnavailable() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:cool_block")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(null); + when(customStack.getDisplayName()).thenReturn(null); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("ns:cool_block"); + String result = hook.blockName(wrapper); + + assertEquals("Cool Block", result); + } + } + + @Test + @DisplayName("Falls back to formatted block ID when CustomStack is null") + void blockName_returnsFormattedId_whenCustomStackNull() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + mocked.when(() -> CustomStack.getInstance("ns:cool_block")).thenReturn(null); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("ns:cool_block"); + String result = hook.blockName(wrapper); + + assertEquals("Cool Block", result); + } + } + + @Test + @DisplayName("Returns formatted material name when wrapper has no custom block") + void blockName_returnsFormattedMaterial_whenVanilla() { + CustomBlockWrapper wrapper = new TestVanillaBlockWrapper(Material.STONE_BRICKS); + String result = hook.blockName(wrapper); + + assertEquals("Stone Bricks", result); + } + + @Test + @DisplayName("Formats block ID without namespace correctly") + void blockName_formatsIdWithoutNamespace() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + mocked.when(() -> CustomStack.getInstance("simple_block")).thenReturn(null); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("simple_block"); + String result = hook.blockName(wrapper); + + assertEquals("Simple Block", result); + } + } + } + + private static class TestCustomItemWrapper extends CustomItemWrapper { + + private final String testCustomItem; + + TestCustomItemWrapper(@NotNull String customItem) { + super(Material.STONE); + this.testCustomItem = customItem; + } + + @NotNull + @Override + public Optional customItem() { + return Optional.of(testCustomItem); + } + + @NotNull + @Override + public Optional material() { + return Optional.empty(); + } + } + + private static class TestVanillaItemWrapper extends CustomItemWrapper { + + private final Material testMaterial; + + TestVanillaItemWrapper(@NotNull Material material) { + super(material); + this.testMaterial = material; + } + + @NotNull + @Override + public Optional customItem() { + return Optional.empty(); + } + + @NotNull + @Override + public Optional material() { + return Optional.of(testMaterial); + } + } + + private static class TestCustomBlockWrapper extends CustomBlockWrapper { + + private final String testCustomBlock; + + TestCustomBlockWrapper(@NotNull String customBlock) { + super(Material.STONE); + this.testCustomBlock = customBlock; + } + + @NotNull + @Override + public Optional customBlock() { + return Optional.of(testCustomBlock); + } + + @NotNull + @Override + public Optional material() { + return Optional.empty(); + } + } + + private static class TestVanillaBlockWrapper extends CustomBlockWrapper { + + private final Material testMaterial; + + TestVanillaBlockWrapper(@NotNull Material material) { + super(material); + this.testMaterial = material; + } + + @NotNull + @Override + public Optional customBlock() { + return Optional.empty(); + } + + @NotNull + @Override + public Optional material() { + return Optional.of(testMaterial); + } + } +} From 11de471b853e48295aedb2c79f58c7c2bebfde48 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 09:52:21 +0000 Subject: [PATCH 2/2] Add success-path test for placeCustomBlock using spy Add a spy-based test that verifies CustomBlock.place() is called when the isCustomBlock(String) guard passes. This covers the happy path of placeCustomBlock without modifying the production code, where isCustomBlock(String) currently returns false for all inputs. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_018B4s5X96MdWqpXpW83hex1 --- .../itemsadder/CoreItemsAdderHookTest.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java b/src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java index 437ce23..1cb5dd1 100644 --- a/src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java +++ b/src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java @@ -35,8 +35,10 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -339,6 +341,20 @@ void placeCustomBlock_throwsIllegalArgument_whenNotValidBlock() { assertThrows(IllegalArgumentException.class, () -> hook.placeCustomBlock(location, "invalid_block")); } + + @Test + @DisplayName("Places block via CustomBlock.place when blockId passes validation") + void placeCustomBlock_placesBlock_whenBlockIsValid() { + Location location = mock(Location.class); + CoreItemsAdderHook spyHook = spy(hook); + doReturn(true).when(spyHook).isCustomBlock("ns:valid_block"); + + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + spyHook.placeCustomBlock(location, "ns:valid_block"); + + mocked.verify(() -> CustomBlock.place("ns:valid_block", location)); + } + } } @Nested