diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f4cfb86 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,62 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +ControlPanel is a BentoBox addon for Minecraft (Paper) that provides customizable GUI menus for players to execute common commands. It supports multiple gamemodes (AcidIsland, BSkyBlock, CaveBlock, SkyGrid, AOneBlock) and 13 localizations. + +## Build Commands + +```bash +# Build the project +mvn clean package + +# Run tests +mvn clean test + +# Run tests and SonarCloud analysis (as CI does) +mvn -B verify + +# Run a single test class +mvn test -Dtest=UtilsTest + +# Build without tests +mvn clean package -DskipTests +``` + +**Requires Java 21.** The output JAR is placed in `target/`. + +## Testing + +Tests use **JUnit 5 + Mockito 5 + MockBukkit**. All integration tests extend `CommonTestSetup` which provides pre-configured mocks for BentoBox, Bukkit, Player, World, Island, and other framework objects. + +Key test infrastructure in `src/test/java/world/bentobox/controlpanel/`: +- `CommonTestSetup.java` — abstract base class with `@BeforeEach`/`@AfterEach` lifecycle, static mocks for `Bukkit` and `Util` +- `WhiteBox.java` — reflection utility for setting private static fields (e.g., BentoBox singleton) +- `TestWorldSettings.java` — stub `WorldSettings` implementation for tests + +For tests requiring database access, mock `DatabaseSetup` statically and use `WhiteBox.setInternalState(Database.class, "databaseSetup", dbSetup)` to inject the mock handler. + +## Architecture + +This is a **BentoBox Addon** — it extends BentoBox's `Addon` class and follows its lifecycle (`onLoad` → `onEnable` → `onReload` → `onDisable`). + +**Entry point:** `ControlPanelAddon.java` — registers commands, loads settings, initializes the manager. + +**Key flow:** +1. `ControlPanelAddon` hooks `PlayerCommand` and `AdminCommand` into each registered BentoBox gamemode's command tree +2. `ControlPanelManager` loads/saves `ControlPanelObject` data from YAML files and caches them per-world +3. `ControlPanelGenerator` builds the GUI using BentoBox's `PanelBuilder` API, resolving button actions, permissions, and placeholders (`[player]`, `[server]`, `[label]`) + +**Data model:** `ControlPanelObject` (implements `DataObject`) represents a single control panel with its button layout. The manager parses the YAML template (`controlPanelTemplate.yml`) into these objects. + +**Configuration:** `Settings.java` uses BentoBox's `@StoreAt`/`@ConfigEntry` annotations for YAML-backed config. The `config.yml` controls which gamemodes the addon is disabled for. + +**Localization:** 13 locale YAML files in `src/main/resources/locales/`. Translation key constants are centralized in `Constants.java`. + +**Optional integration:** `ItemsAdderParse.java` provides soft-dependency support for the ItemsAdder plugin's custom items. + +## CI + +GitHub Actions on push to `develop` and PRs: builds with Maven + runs SonarCloud analysis (project: `BentoBoxWorld_ControlPanel`). diff --git a/pom.xml b/pom.xml index cd29469..a034c5e 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,9 @@ BentoBoxWorld_ControlPanel bentobox-world https://sonarcloud.io + + 5.10.2 + 5.11.0 @@ -104,6 +107,14 @@ + + + jitpack.io + https://jitpack.io + + true + + papermc https://repo.papermc.io/repository/maven-public/ @@ -128,6 +139,36 @@ + + com.github.MockBukkit + MockBukkit + v1.21-SNAPSHOT + test + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + io.papermc.paper paper-api @@ -221,11 +262,12 @@ org.apache.maven.plugins maven-surefire-plugin - 3.1.2 + 3.5.4 ${argLine} + -XX:+EnableDynamicAgentLoading --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED @@ -307,7 +349,7 @@ org.jacoco jacoco-maven-plugin - 0.8.10 + 0.8.13 true diff --git a/src/test/java/world/bentobox/controlpanel/CommonTestSetup.java b/src/test/java/world/bentobox/controlpanel/CommonTestSetup.java new file mode 100644 index 0000000..e80d7a0 --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/CommonTestSetup.java @@ -0,0 +1,274 @@ +package world.bentobox.controlpanel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.Player.Spigot; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.inventory.ItemFactory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.metadata.FixedMetadataValue; +import org.bukkit.metadata.MetadataValue; +import org.bukkit.plugin.PluginManager; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.util.Vector; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; + +import com.google.common.collect.ImmutableSet; + +import net.md_5.bungee.api.chat.TextComponent; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.configuration.WorldSettings; +import world.bentobox.bentobox.api.user.Notifier; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.database.objects.Players; +import world.bentobox.bentobox.managers.BlueprintsManager; +import world.bentobox.bentobox.managers.FlagsManager; +import world.bentobox.bentobox.managers.HooksManager; +import world.bentobox.bentobox.managers.IslandWorldManager; +import world.bentobox.bentobox.managers.IslandsManager; +import world.bentobox.bentobox.managers.LocalesManager; +import world.bentobox.bentobox.managers.PlaceholdersManager; +import world.bentobox.bentobox.managers.PlayersManager; +import world.bentobox.bentobox.util.Util; + +/** + * Common items for testing. Don't forget to use super.setUp()! + *

+ * Sets up BentoBox plugin, pluginManager and ItemFactory. + * Location, world, playersManager and player. + * IWM, Addon and WorldSettings. IslandManager with one + * island with protection and nothing allowed by default. + * Owner of island is player with same UUID. + * Locales, placeholders. + */ +public abstract class CommonTestSetup { + + protected UUID uuid = UUID.randomUUID(); + + @Mock + protected Player mockPlayer; + @Mock + protected PluginManager pim; + @Mock + protected ItemFactory itemFactory; + @Mock + protected Location location; + @Mock + protected World world; + @Mock + protected IslandWorldManager iwm; + @Mock + protected IslandsManager im; + @Mock + protected Island island; + @Mock + protected BentoBox plugin; + @Mock + protected PlayerInventory inv; + @Mock + protected Notifier notifier; + @Mock + protected FlagsManager fm; + @Mock + protected Spigot spigot; + @Mock + protected HooksManager hooksManager; + @Mock + protected BlueprintsManager bm; + + protected ServerMock server; + + protected MockedStatic mockedBukkit; + protected MockedStatic mockedUtil; + + protected AutoCloseable closeable; + + @Mock + protected BukkitScheduler sch; + @Mock + protected LocalesManager lm; + + @Mock + protected PlaceholdersManager phm; + + + @BeforeEach + public void setUp() throws Exception { + closeable = MockitoAnnotations.openMocks(this); + server = MockBukkit.mock(); + // Set up plugin + WhiteBox.setInternalState(BentoBox.class, "instance", plugin); + + // Bukkit static mock + mockedBukkit = Mockito.mockStatic(Bukkit.class, Mockito.RETURNS_DEEP_STUBS); + mockedBukkit.when(Bukkit::getMinecraftVersion).thenReturn("1.21.10"); + mockedBukkit.when(Bukkit::getBukkitVersion).thenReturn(""); + mockedBukkit.when(Bukkit::getPluginManager).thenReturn(pim); + mockedBukkit.when(Bukkit::getItemFactory).thenReturn(itemFactory); + mockedBukkit.when(Bukkit::getServer).thenReturn(server); + // Location + when(location.getWorld()).thenReturn(world); + when(location.getBlockX()).thenReturn(0); + when(location.getBlockY()).thenReturn(0); + when(location.getBlockZ()).thenReturn(0); + when(location.toVector()).thenReturn(new Vector(0, 0, 0)); + when(location.clone()).thenReturn(location); + + // Players Manager and meta data + PlayersManager pm = mock(PlayersManager.class); + when(plugin.getPlayers()).thenReturn(pm); + Players players = mock(Players.class); + when(players.getMetaData()).thenReturn(Optional.empty()); + when(pm.getPlayer(any(UUID.class))).thenReturn(players); + + // Player + when(mockPlayer.getUniqueId()).thenReturn(uuid); + when(mockPlayer.getLocation()).thenReturn(location); + when(mockPlayer.getWorld()).thenReturn(world); + when(mockPlayer.getName()).thenReturn("tastybento"); + when(mockPlayer.getInventory()).thenReturn(inv); + when(mockPlayer.spigot()).thenReturn(spigot); + when(mockPlayer.getType()).thenReturn(EntityType.PLAYER); + + User.setPlugin(plugin); + User.clearUsers(); + User.getInstance(mockPlayer); + + // IWM + when(plugin.getIWM()).thenReturn(iwm); + when(iwm.inWorld(any(Location.class))).thenReturn(true); + when(iwm.inWorld(any(World.class))).thenReturn(true); + when(iwm.getFriendlyName(any())).thenReturn("BSkyBlock"); + when(iwm.getAddon(any())).thenReturn(Optional.empty()); + + // World Settings + WorldSettings worldSet = new TestWorldSettings(); + when(iwm.getWorldSettings(any())).thenReturn(worldSet); + + // Island Manager + when(plugin.getIslands()).thenReturn(im); + Optional optionalIsland = Optional.of(island); + when(im.getProtectedIslandAt(any())).thenReturn(optionalIsland); + + // Island - nothing is allowed by default + when(island.isAllowed(any())).thenReturn(false); + when(island.isAllowed(any(User.class), any())).thenReturn(false); + when(island.getOwner()).thenReturn(uuid); + when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid)); + + // Enable reporting from Flags class + MetadataValue mdv = new FixedMetadataValue(plugin, "_why_debug"); + when(mockPlayer.getMetadata(anyString())).thenReturn(Collections.singletonList(mdv)); + + // Locales & Placeholders + when(lm.get(any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + when(plugin.getPlaceholdersManager()).thenReturn(phm); + when(phm.replacePlaceholders(any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + when(plugin.getLocalesManager()).thenReturn(lm); + // Notifier + when(plugin.getNotifier()).thenReturn(notifier); + + // Fake players + world.bentobox.bentobox.Settings settings = new world.bentobox.bentobox.Settings(); + when(plugin.getSettings()).thenReturn(settings); + + // Util + mockedUtil = Mockito.mockStatic(Util.class, Mockito.CALLS_REAL_METHODS); + mockedUtil.when(() -> Util.getWorld(any())).thenReturn(mock(World.class)); + Util.setPlugin(plugin); + + mockedUtil.when(() -> Util.findFirstMatchingEnum(any(), any())).thenCallRealMethod(); + + // Server & Scheduler + mockedBukkit.when(Bukkit::getScheduler).thenReturn(sch); + + // Hooks + when(hooksManager.getHook(anyString())).thenReturn(Optional.empty()); + when(plugin.getHooks()).thenReturn(hooksManager); + + // Blueprints Manager + when(plugin.getBlueprintsManager()).thenReturn(bm); + } + + @AfterEach + public void tearDown() throws Exception { + mockedBukkit.closeOnDemand(); + mockedUtil.closeOnDemand(); + closeable.close(); + MockBukkit.unmock(); + User.clearUsers(); + Mockito.framework().clearInlineMocks(); + deleteAll(new File("database")); + deleteAll(new File("database_backup")); + } + + protected static void deleteAll(File file) throws IOException { + if (file.exists()) { + Files.walk(file.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + } + + public void checkSpigotMessage(String expectedMessage) { + checkSpigotMessage(expectedMessage, 1); + } + + @SuppressWarnings("deprecation") + public void checkSpigotMessage(String expectedMessage, int expectedOccurrences) { + ArgumentCaptor captor = ArgumentCaptor.forClass(TextComponent.class); + verify(spigot, atLeast(0)).sendMessage(captor.capture()); + + List capturedMessages = captor.getAllValues(); + long actualOccurrences = capturedMessages.stream().map(component -> component.toLegacyText()) + .filter(messageText -> messageText.contains(expectedMessage)) + .count(); + + assertEquals(expectedOccurrences, + actualOccurrences, "Expected message occurrence mismatch: " + expectedMessage); + } + + public EntityExplodeEvent getExplodeEvent(Entity entity, Location l, List list) { + return new EntityExplodeEvent(entity, l, list, 0, null); + } + + public PlayerDeathEvent getPlayerDeathEvent(Player player, List drops, int droppedExp, int newExp, + int newTotalExp, int newLevel, @Nullable String deathMessage) { + return new PlayerDeathEvent(player, null, drops, droppedExp, newExp, + newTotalExp, newLevel, deathMessage); + } +} diff --git a/src/test/java/world/bentobox/controlpanel/ControlPanelAddonTest.java b/src/test/java/world/bentobox/controlpanel/ControlPanelAddonTest.java new file mode 100644 index 0000000..1e80600 --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/ControlPanelAddonTest.java @@ -0,0 +1,220 @@ +package world.bentobox.controlpanel; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import world.bentobox.bentobox.api.addons.Addon.State; +import world.bentobox.bentobox.api.addons.AddonDescription; +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.configuration.Config; +import world.bentobox.bentobox.database.AbstractDatabaseHandler; +import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.database.DatabaseSetup; +import world.bentobox.bentobox.managers.AddonsManager; +import world.bentobox.controlpanel.config.Settings; +import world.bentobox.controlpanel.managers.ControlPanelManager; + +class ControlPanelAddonTest extends CommonTestSetup { + + private ControlPanelAddon addon; + + @Mock + private AddonsManager addonsManager; + + private MockedStatic mockedDbSetup; + + @SuppressWarnings("unchecked") + @BeforeEach + @Override + public void setUp() throws Exception { + super.setUp(); + addon = new ControlPanelAddon(); + + // Set up data folder + File dataFolder = new File("addons/ControlPanel"); + dataFolder.mkdirs(); + addon.setDataFolder(dataFolder); + + // Create JAR file with config.yml + File jFile = new File("addon.jar"); + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jFile))) { + // Add config.yml + Path configSrc = Paths.get("src/main/resources/config.yml"); + if (Files.exists(configSrc)) { + Path configDest = Paths.get("config.yml"); + Files.copy(configSrc, configDest, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + addToJar(configDest, jos); + Files.deleteIfExists(configDest); + } + // Add controlPanelTemplate.yml + Path templateSrc = Paths.get("src/main/resources/controlPanelTemplate.yml"); + if (Files.exists(templateSrc)) { + Path templateDest = Paths.get("controlPanelTemplate.yml"); + Files.copy(templateSrc, templateDest, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + addToJar(templateDest, jos); + Files.deleteIfExists(templateDest); + } + } + addon.setFile(jFile); + + AddonDescription desc = new AddonDescription.Builder("main", "ControlPanel", "1.16.0") + .description("test").authors("tastybento").build(); + addon.setDescription(desc); + + when(plugin.getAddonsManager()).thenReturn(addonsManager); + when(addonsManager.getGameModeAddons()).thenReturn(Collections.emptyList()); + when(plugin.isEnabled()).thenReturn(true); + when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger()); + when(plugin.getFlagsManager()).thenReturn(fm); + when(fm.getFlags()).thenReturn(Collections.emptyList()); + + // Mock DatabaseSetup for ControlPanelManager creation + AbstractDatabaseHandler handler = mock(AbstractDatabaseHandler.class); + when(handler.loadObjects()).thenReturn(Collections.emptyList()); + when(handler.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true)); + DatabaseSetup dbSetup = mock(DatabaseSetup.class); + when(dbSetup.getHandler(any())).thenReturn(handler); + mockedDbSetup = Mockito.mockStatic(DatabaseSetup.class); + mockedDbSetup.when(() -> DatabaseSetup.getDatabase()).thenReturn(dbSetup); + WhiteBox.setInternalState(Database.class, "databaseSetup", dbSetup); + } + + @AfterEach + @Override + public void tearDown() throws Exception { + mockedDbSetup.closeOnDemand(); + super.tearDown(); + deleteAll(new File("addons")); + Files.deleteIfExists(Paths.get("addon.jar")); + } + + private void addToJar(Path path, JarOutputStream jos) throws IOException { + try (FileInputStream fis = new FileInputStream(path.toFile())) { + byte[] buffer = new byte[1024]; + int bytesRead; + JarEntry entry = new JarEntry(path.toString()); + jos.putNextEntry(entry); + while ((bytesRead = fis.read(buffer)) != -1) { + jos.write(buffer, 0, bytesRead); + } + } + } + + @Test + void testOnLoadSuccess() { + Settings settings = new Settings(); + try (MockedConstruction mockedConfig = Mockito.mockConstruction(Config.class, + (mock, context) -> when(mock.loadConfigObject()).thenReturn(settings))) { + addon.onLoad(); + assertNotNull(addon.getSettings()); + } + } + + @Test + void testOnLoadFailNullSettings() { + try (MockedConstruction mockedConfig = Mockito.mockConstruction(Config.class, + (mock, context) -> when(mock.loadConfigObject()).thenReturn(null))) { + addon.onLoad(); + assertNull(addon.getSettings()); + } + } + + @Test + void testOnEnableWithGameModes() { + Settings settings = new Settings(); + try (MockedConstruction mockedConfig = Mockito.mockConstruction(Config.class, + (mock, context) -> when(mock.loadConfigObject()).thenReturn(settings))) { + addon.onLoad(); + } + + GameModeAddon gma = mock(GameModeAddon.class); + AddonDescription gmaDesc = new AddonDescription.Builder("main", "BSkyBlock", "1.0").build(); + when(gma.getDescription()).thenReturn(gmaDesc); + + CompositeCommand playerCmd = mock(CompositeCommand.class); + when(playerCmd.getSubCommandAliases()).thenReturn(new HashMap<>()); + when(playerCmd.getAddon()).thenReturn(addon); + when(gma.getPlayerCommand()).thenReturn(Optional.of(playerCmd)); + + CompositeCommand adminCmd = mock(CompositeCommand.class); + when(adminCmd.getSubCommandAliases()).thenReturn(new HashMap<>()); + when(adminCmd.getAddon()).thenReturn(addon); + when(gma.getAdminCommand()).thenReturn(Optional.of(adminCmd)); + + when(addonsManager.getGameModeAddons()).thenReturn(Collections.singletonList(gma)); + + addon.setState(State.ENABLED); + addon.onEnable(); + assertNotNull(addon.getAddonManager()); + } + + @Test + void testOnReloadSuccess() { + Settings settings = new Settings(); + try (MockedConstruction mockedConfig = Mockito.mockConstruction(Config.class, + (mock, context) -> when(mock.loadConfigObject()).thenReturn(settings))) { + addon.onLoad(); + } + addon.setState(State.ENABLED); + addon.onEnable(); + + Settings newSettings = new Settings(); + try (MockedConstruction mockedConfig = Mockito.mockConstruction(Config.class, + (mock, context) -> when(mock.loadConfigObject()).thenReturn(newSettings))) { + addon.onReload(); + assertNotNull(addon.getSettings()); + } + } + + @Test + void testOnDisable() { + Settings settings = new Settings(); + try (MockedConstruction mockedConfig = Mockito.mockConstruction(Config.class, + (mock, context) -> { + when(mock.loadConfigObject()).thenReturn(settings); + when(mock.saveConfigObject(any())).thenReturn(true); + })) { + addon.onLoad(); + addon.setState(State.ENABLED); + addon.onEnable(); + addon.onDisable(); + } + } + + @Test + void testGetSettingsDefault() { + assertNull(addon.getSettings()); + } + + @Test + void testGetAddonManagerDefault() { + assertNull(addon.getAddonManager()); + } +} diff --git a/src/test/java/world/bentobox/controlpanel/TestWorldSettings.java b/src/test/java/world/bentobox/controlpanel/TestWorldSettings.java new file mode 100644 index 0000000..4b9e027 --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/TestWorldSettings.java @@ -0,0 +1,341 @@ +package world.bentobox.controlpanel; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.bukkit.Difficulty; +import org.bukkit.GameMode; +import org.bukkit.entity.EntityType; +import org.eclipse.jdt.annotation.NonNull; + +import world.bentobox.bentobox.api.configuration.WorldSettings; +import world.bentobox.bentobox.api.flags.Flag; + +/** + * Class for tests that require world settings + */ +public class TestWorldSettings implements WorldSettings { + + private long epoch; + + @Override + public GameMode getDefaultGameMode() { + return GameMode.SURVIVAL; + } + + @Override + public Map getDefaultIslandFlags() { + return Collections.emptyMap(); + } + + @Override + public Map getDefaultIslandSettings() { + return Collections.emptyMap(); + } + + @Override + public Difficulty getDifficulty() { + return Difficulty.EASY; + } + + @Override + public void setDifficulty(Difficulty difficulty) { + } + + @Override + public String getFriendlyName() { + return "friendly_name"; + } + + @Override + public int getIslandDistance() { + return 0; + } + + @Override + public int getIslandHeight() { + return 0; + } + + @Override + public int getIslandProtectionRange() { + return 0; + } + + @Override + public int getIslandStartX() { + return 0; + } + + @Override + public int getIslandStartZ() { + return 0; + } + + @Override + public int getIslandXOffset() { + return 0; + } + + @Override + public int getIslandZOffset() { + return 0; + } + + @Override + public List getIvSettings() { + return Collections.emptyList(); + } + + @Override + public int getMaxHomes() { + return 3; + } + + @Override + public int getMaxIslands() { + return 0; + } + + @Override + public int getMaxTeamSize() { + return 4; + } + + @Override + public int getNetherSpawnRadius() { + return 10; + } + + @Override + public String getPermissionPrefix() { + return "perm."; + } + + @Override + public Set getRemoveMobsWhitelist() { + return Collections.emptySet(); + } + + @Override + public int getSeaHeight() { + return 0; + } + + @Override + public List getHiddenFlags() { + return Collections.emptyList(); + } + + @Override + public List getVisitorBannedCommands() { + return Collections.emptyList(); + } + + @Override + public Map getWorldFlags() { + return Collections.emptyMap(); + } + + @Override + public String getWorldName() { + return "world_name"; + } + + @Override + public boolean isDragonSpawn() { + return false; + } + + @Override + public boolean isEndGenerate() { + return true; + } + + @Override + public boolean isEndIslands() { + return true; + } + + @Override + public boolean isNetherGenerate() { + return true; + } + + @Override + public boolean isNetherIslands() { + return true; + } + + @Override + public boolean isOnJoinResetEnderChest() { + return false; + } + + @Override + public boolean isOnJoinResetInventory() { + return false; + } + + @Override + public boolean isOnJoinResetMoney() { + return false; + } + + @Override + public boolean isOnJoinResetHealth() { + return false; + } + + @Override + public boolean isOnJoinResetHunger() { + return false; + } + + @Override + public boolean isOnJoinResetXP() { + return false; + } + + @Override + public @NonNull List getOnJoinCommands() { + return Collections.emptyList(); + } + + @Override + public boolean isOnLeaveResetEnderChest() { + return false; + } + + @Override + public boolean isOnLeaveResetInventory() { + return false; + } + + @Override + public boolean isOnLeaveResetMoney() { + return false; + } + + @Override + public boolean isOnLeaveResetHealth() { + return false; + } + + @Override + public boolean isOnLeaveResetHunger() { + return false; + } + + @Override + public boolean isOnLeaveResetXP() { + return false; + } + + @Override + public @NonNull List getOnLeaveCommands() { + return Collections.emptyList(); + } + + @Override + public boolean isUseOwnGenerator() { + return false; + } + + @Override + public boolean isWaterUnsafe() { + return false; + } + + @Override + public List getGeoLimitSettings() { + return Collections.emptyList(); + } + + @Override + public int getResetLimit() { + return 0; + } + + @Override + public long getResetEpoch() { + return epoch; + } + + @Override + public void setResetEpoch(long timestamp) { + this.epoch = timestamp; + } + + @Override + public boolean isTeamJoinDeathReset() { + return false; + } + + @Override + public int getDeathsMax() { + return 0; + } + + @Override + public boolean isDeathsCounted() { + return true; + } + + @Override + public boolean isDeathsResetOnNewIsland() { + return true; + } + + @Override + public boolean isAllowSetHomeInNether() { + return false; + } + + @Override + public boolean isAllowSetHomeInTheEnd() { + return false; + } + + @Override + public boolean isRequireConfirmationToSetHomeInNether() { + return false; + } + + @Override + public boolean isRequireConfirmationToSetHomeInTheEnd() { + return false; + } + + @Override + public int getBanLimit() { + return 10; + } + + @Override + public boolean isLeaversLoseReset() { + return true; + } + + @Override + public boolean isKickedKeepInventory() { + return true; + } + + @Override + public boolean isCreateIslandOnFirstLoginEnabled() { + return false; + } + + @Override + public int getCreateIslandOnFirstLoginDelay() { + return 0; + } + + @Override + public boolean isCreateIslandOnFirstLoginAbortOnLogout() { + return false; + } +} diff --git a/src/test/java/world/bentobox/controlpanel/WhiteBox.java b/src/test/java/world/bentobox/controlpanel/WhiteBox.java new file mode 100644 index 0000000..36871de --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/WhiteBox.java @@ -0,0 +1,19 @@ +package world.bentobox.controlpanel; + +public class WhiteBox { + /** + * Sets the value of a private static field using Java Reflection. + * @param targetClass The class containing the static field. + * @param fieldName The name of the private static field. + * @param value The value to set the field to. + */ + public static void setInternalState(Class targetClass, String fieldName, Object value) { + try { + java.lang.reflect.Field field = targetClass.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(null, value); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("Failed to set static field '" + fieldName + "' on class " + targetClass.getName(), e); + } + } +} diff --git a/src/test/java/world/bentobox/controlpanel/commands/admin/AdminCommandTest.java b/src/test/java/world/bentobox/controlpanel/commands/admin/AdminCommandTest.java new file mode 100644 index 0000000..ca093ab --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/commands/admin/AdminCommandTest.java @@ -0,0 +1,51 @@ +package world.bentobox.controlpanel.commands.admin; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.HashMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.controlpanel.CommonTestSetup; +import world.bentobox.controlpanel.ControlPanelAddon; +import world.bentobox.controlpanel.managers.ControlPanelManager; + +class AdminCommandTest extends CommonTestSetup { + + private AdminCommand command; + + @Mock + private ControlPanelAddon addon; + @Mock + private CompositeCommand parentCommand; + @Mock + private ControlPanelManager manager; + + @BeforeEach + @Override + public void setUp() throws Exception { + super.setUp(); + + when(parentCommand.getSubCommandAliases()).thenReturn(new HashMap<>()); + when(parentCommand.getWorld()).thenReturn(world); + when(parentCommand.getAddon()).thenReturn(addon); + when(parentCommand.getTopLabel()).thenReturn("bsb"); + when(parentCommand.getPermissionPrefix()).thenReturn("bskyblock."); + + when(addon.getAddonManager()).thenReturn(manager); + + command = new AdminCommand(addon, parentCommand); + } + + @Test + void testExecuteShowsHelp() { + User user = User.getInstance(mockPlayer); + assertTrue(command.execute(user, "controlpanel", Collections.emptyList())); + } +} diff --git a/src/test/java/world/bentobox/controlpanel/commands/user/PlayerCommandTest.java b/src/test/java/world/bentobox/controlpanel/commands/user/PlayerCommandTest.java new file mode 100644 index 0000000..52f74bd --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/commands/user/PlayerCommandTest.java @@ -0,0 +1,105 @@ +package world.bentobox.controlpanel.commands.user; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Optional; + +import org.bukkit.World; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.controlpanel.CommonTestSetup; +import world.bentobox.controlpanel.ControlPanelAddon; +import world.bentobox.controlpanel.database.objects.ControlPanelObject; +import world.bentobox.controlpanel.managers.ControlPanelManager; +import world.bentobox.controlpanel.panels.ControlPanelGenerator; + +class PlayerCommandTest extends CommonTestSetup { + + private PlayerCommand command; + + @Mock + private ControlPanelAddon addon; + @Mock + private CompositeCommand parentCommand; + @Mock + private ControlPanelManager manager; + @Mock + private ControlPanelObject panelObject; + + @BeforeEach + @Override + public void setUp() throws Exception { + super.setUp(); + + when(parentCommand.getSubCommandAliases()).thenReturn(new HashMap<>()); + when(parentCommand.getWorld()).thenReturn(world); + when(parentCommand.getAddon()).thenReturn(addon); + when(parentCommand.getTopLabel()).thenReturn("island"); + when(parentCommand.getPermissionPrefix()).thenReturn("bskyblock."); + + when(addon.getAddonManager()).thenReturn(manager); + + command = new PlayerCommand(addon, parentCommand); + } + + @Test + void testCanExecuteNoPanelNonOp() { + User user = User.getInstance(mockPlayer); + when(mockPlayer.isOp()).thenReturn(false); + when(manager.getUserControlPanel(any(), any(World.class), anyString())).thenReturn(null); + + assertFalse(command.canExecute(user, "controlpanel", Collections.emptyList())); + checkSpigotMessage("controlpanel.errors.no-valid-panels"); + } + + @Test + void testCanExecuteNoPanelOp() { + User user = User.getInstance(mockPlayer); + when(mockPlayer.isOp()).thenReturn(true); + when(manager.getUserControlPanel(any(), any(World.class), anyString())).thenReturn(null); + + // Set up IWM to return a GameModeAddon with admin command + GameModeAddon gma = mock(GameModeAddon.class); + CompositeCommand adminCmd = mock(CompositeCommand.class); + when(adminCmd.getTopLabel()).thenReturn("bsb"); + when(gma.getAdminCommand()).thenReturn(Optional.of(adminCmd)); + when(iwm.getAddon(any())).thenReturn(Optional.of(gma)); + + assertFalse(command.canExecute(user, "controlpanel", Collections.emptyList())); + } + + @Test + void testCanExecuteWithPanel() { + User user = User.getInstance(mockPlayer); + when(manager.getUserControlPanel(any(), any(World.class), anyString())).thenReturn(panelObject); + + assertTrue(command.canExecute(user, "controlpanel", Collections.emptyList())); + } + + @Test + void testExecute() { + User user = User.getInstance(mockPlayer); + when(manager.getUserControlPanel(any(), any(World.class), anyString())).thenReturn(panelObject); + command.canExecute(user, "controlpanel", Collections.emptyList()); + + try (MockedStatic mockedGen = Mockito.mockStatic(ControlPanelGenerator.class)) { + assertTrue(command.execute(user, "controlpanel", Collections.emptyList())); + mockedGen.verify(() -> ControlPanelGenerator.open(any(), any(), any(), anyString())); + } + } +} diff --git a/src/test/java/world/bentobox/controlpanel/config/SettingsTest.java b/src/test/java/world/bentobox/controlpanel/config/SettingsTest.java new file mode 100644 index 0000000..fca8b04 --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/config/SettingsTest.java @@ -0,0 +1,38 @@ +package world.bentobox.controlpanel.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SettingsTest { + + private Settings settings; + + @BeforeEach + void setUp() { + settings = new Settings(); + } + + @Test + void testDefaultDisabledGameModes() { + assertNotNull(settings.getDisabledGameModes()); + assertTrue(settings.getDisabledGameModes().isEmpty()); + } + + @Test + void testSetDisabledGameModes() { + Set modes = new HashSet<>(); + modes.add("BSkyBlock"); + modes.add("AcidIsland"); + settings.setDisabledGameModes(modes); + assertEquals(2, settings.getDisabledGameModes().size()); + assertTrue(settings.getDisabledGameModes().contains("BSkyBlock")); + assertTrue(settings.getDisabledGameModes().contains("AcidIsland")); + } +} diff --git a/src/test/java/world/bentobox/controlpanel/database/objects/ControlPanelObjectTest.java b/src/test/java/world/bentobox/controlpanel/database/objects/ControlPanelObjectTest.java new file mode 100644 index 0000000..c6beb6c --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/database/objects/ControlPanelObjectTest.java @@ -0,0 +1,110 @@ +package world.bentobox.controlpanel.database.objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.Material; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import world.bentobox.controlpanel.database.objects.ControlPanelObject.ControlPanelButton; + +class ControlPanelObjectTest { + + private ControlPanelObject cpo; + + @BeforeEach + void setUp() { + cpo = new ControlPanelObject(); + } + + @Test + void testUniqueId() { + assertNull(cpo.getUniqueId()); + cpo.setUniqueId("BSkyBlock_default"); + assertEquals("BSkyBlock_default", cpo.getUniqueId()); + } + + @Test + void testGameMode() { + assertNull(cpo.getGameMode()); + cpo.setGameMode("BSkyBlock"); + assertEquals("BSkyBlock", cpo.getGameMode()); + } + + @Test + void testPermissionSuffix() { + assertNull(cpo.getPermissionSuffix()); + cpo.setPermissionSuffix("default"); + assertEquals("default", cpo.getPermissionSuffix()); + } + + @Test + void testPanelName() { + assertNull(cpo.getPanelName()); + cpo.setPanelName("&1Commands"); + assertEquals("&1Commands", cpo.getPanelName()); + } + + @Test + void testDefaultPanel() { + assertFalse(cpo.isDefaultPanel()); + cpo.setDefaultPanel(true); + assertTrue(cpo.isDefaultPanel()); + } + + @Test + void testPanelButtons() { + assertNull(cpo.getPanelButtons()); + List buttons = new ArrayList<>(); + cpo.setPanelButtons(buttons); + assertNotNull(cpo.getPanelButtons()); + assertTrue(cpo.getPanelButtons().isEmpty()); + } + + @Test + void testControlPanelButton() { + ControlPanelButton button = new ControlPanelButton(); + + assertEquals(0, button.getSlot()); + button.setSlot(5); + assertEquals(5, button.getSlot()); + + assertNull(button.getMaterial()); + button.setMaterial(Material.GRASS_BLOCK); + assertEquals(Material.GRASS_BLOCK, button.getMaterial()); + + assertNull(button.getIcon()); + + assertNull(button.getName()); + button.setName("Test Button"); + assertEquals("Test Button", button.getName()); + + assertNull(button.getCommand()); + button.setCommand("island go"); + assertEquals("island go", button.getCommand()); + + assertNull(button.getDescriptionLines()); + List desc = new ArrayList<>(); + desc.add("Line 1"); + button.setDescriptionLines(desc); + assertEquals(1, button.getDescriptionLines().size()); + assertEquals("Line 1", button.getDescriptionLines().get(0)); + } + + @SuppressWarnings("deprecation") + @Test + void testControlPanelButtonDeprecatedDescription() { + ControlPanelButton button = new ControlPanelButton(); + assertNull(button.getDescription()); + button.setDescription("old description"); + assertEquals("old description", button.getDescription()); + } +} diff --git a/src/test/java/world/bentobox/controlpanel/managers/ControlPanelManagerTest.java b/src/test/java/world/bentobox/controlpanel/managers/ControlPanelManagerTest.java new file mode 100644 index 0000000..aaf0186 --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/managers/ControlPanelManagerTest.java @@ -0,0 +1,279 @@ +package world.bentobox.controlpanel.managers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; + +import org.bukkit.permissions.PermissionAttachmentInfo; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.AbstractDatabaseHandler; +import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.database.DatabaseSetup; +import world.bentobox.controlpanel.CommonTestSetup; +import world.bentobox.controlpanel.ControlPanelAddon; +import world.bentobox.controlpanel.WhiteBox; +import world.bentobox.controlpanel.database.objects.ControlPanelObject; +import world.bentobox.controlpanel.database.objects.ControlPanelObject.ControlPanelButton; +import world.bentobox.controlpanel.utils.Utils; + +@SuppressWarnings("unchecked") +class ControlPanelManagerTest extends CommonTestSetup { + + private ControlPanelManager manager; + + @Mock + private ControlPanelAddon addon; + + private MockedStatic mockedUtils; + private MockedStatic mockedDbSetup; + private AbstractDatabaseHandler handler; + + @BeforeEach + @Override + public void setUp() throws Exception { + super.setUp(); + + when(addon.getPlugin()).thenReturn(plugin); + when(addon.getLogger()).thenReturn(Logger.getAnonymousLogger()); + + File dataFolder = new File("addons/ControlPanel"); + dataFolder.mkdirs(); + when(addon.getDataFolder()).thenReturn(dataFolder); + + // Create the template file so the constructor doesn't try to save resources + File templateFile = new File(dataFolder, "controlPanelTemplate.yml"); + if (!templateFile.exists()) { + templateFile.createNewFile(); + } + + // Mock Utils static methods + mockedUtils = Mockito.mockStatic(Utils.class); + mockedUtils.when(() -> Utils.getGameMode(any(org.bukkit.World.class))).thenReturn("BSkyBlock"); + mockedUtils.when(() -> Utils.getGameMode(any(GameModeAddon.class))).thenReturn("BSkyBlock"); + mockedUtils.when(() -> Utils.getPermissionValue(any(), anyString(), any())).thenCallRealMethod(); + mockedUtils.when(() -> Utils.readIntArray(any())).thenCallRealMethod(); + + // Mock DatabaseSetup to return a mock handler + handler = mock(AbstractDatabaseHandler.class); + when(handler.loadObjects()).thenReturn(Collections.emptyList()); + when(handler.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true)); + DatabaseSetup dbSetup = mock(DatabaseSetup.class); + when(dbSetup.getHandler(any())).thenReturn(handler); + mockedDbSetup = Mockito.mockStatic(DatabaseSetup.class); + mockedDbSetup.when(() -> DatabaseSetup.getDatabase()).thenReturn(dbSetup); + // Also set the static field directly since it may have been initialized already + WhiteBox.setInternalState(Database.class, "databaseSetup", dbSetup); + + manager = new ControlPanelManager(addon); + } + + @AfterEach + @Override + public void tearDown() throws Exception { + mockedUtils.closeOnDemand(); + mockedDbSetup.closeOnDemand(); + super.tearDown(); + deleteAll(new File("addons")); + } + + @Test + void testConstructor() { + assertNotNull(manager); + } + + @Test + void testLoadWithObjects() throws Exception { + ControlPanelObject cpo = createDefaultPanel(); + when(handler.loadObjects()).thenReturn(Collections.singletonList(cpo)); + + manager.reload(); + assertTrue(manager.hasAnyControlPanel(world)); + } + + @Test + void testLoadMigratesNullName() throws Exception { + ControlPanelObject cpo = new ControlPanelObject(); + cpo.setUniqueId("BSkyBlock_default"); + cpo.setGameMode("BSkyBlock"); + cpo.setDefaultPanel(true); + + ControlPanelButton button = new ControlPanelButton(); + button.setSlot(0); + button.setName(null); + button.setCommand("test command"); + button.setDescriptionLines(new ArrayList<>()); + cpo.setPanelButtons(new ArrayList<>(List.of(button))); + + when(handler.loadObjects()).thenReturn(Collections.singletonList(cpo)); + + manager.reload(); + assertTrue(manager.hasAnyControlPanel(world)); + } + + @SuppressWarnings("deprecation") + @Test + void testLoadMigratesDeprecatedDescription() throws Exception { + ControlPanelObject cpo = new ControlPanelObject(); + cpo.setUniqueId("BSkyBlock_default"); + cpo.setGameMode("BSkyBlock"); + cpo.setDefaultPanel(true); + + ControlPanelButton button = new ControlPanelButton(); + button.setSlot(0); + button.setName("Test"); + button.setCommand("test"); + button.setDescription("old single description"); + button.setDescriptionLines(null); + cpo.setPanelButtons(new ArrayList<>(List.of(button))); + + when(handler.loadObjects()).thenReturn(Collections.singletonList(cpo)); + + manager.reload(); + assertTrue(manager.hasAnyControlPanel(world)); + } + + @Test + void testHasAnyControlPanelEmpty() { + assertFalse(manager.hasAnyControlPanel(world)); + } + + @Test + void testHasAnyControlPanelEmptyGameMode() { + mockedUtils.when(() -> Utils.getGameMode(any(org.bukkit.World.class))).thenReturn(""); + assertFalse(manager.hasAnyControlPanel(world)); + } + + @Test + void testHasAnyControlPanelGameModeAddon() { + GameModeAddon gma = mock(GameModeAddon.class); + assertFalse(manager.hasAnyControlPanel(gma)); + } + + @Test + void testGetUserControlPanelNoPermission() { + User user = User.getInstance(mockPlayer); + when(mockPlayer.getEffectivePermissions()).thenReturn(Collections.emptySet()); + + ControlPanelObject result = manager.getUserControlPanel(user, world, "bskyblock."); + assertNull(result); + } + + @Test + void testGetUserControlPanelDefaultFallback() throws Exception { + ControlPanelObject cpo = createDefaultPanel(); + when(handler.loadObjects()).thenReturn(Collections.singletonList(cpo)); + manager.reload(); + + User user = User.getInstance(mockPlayer); + when(mockPlayer.getEffectivePermissions()).thenReturn(Collections.emptySet()); + + ControlPanelObject result = manager.getUserControlPanel(user, world, "bskyblock."); + assertNotNull(result); + assertTrue(result.isDefaultPanel()); + } + + @Test + void testGetUserControlPanelWithPermission() throws Exception { + ControlPanelObject cpo = new ControlPanelObject(); + cpo.setUniqueId("BSkyBlock_vip"); + cpo.setGameMode("BSkyBlock"); + cpo.setPermissionSuffix("vip"); + cpo.setDefaultPanel(false); + cpo.setPanelButtons(new ArrayList<>()); + + when(handler.loadObjects()).thenReturn(Collections.singletonList(cpo)); + manager.reload(); + + User user = User.getInstance(mockPlayer); + PermissionAttachmentInfo pai = mock(PermissionAttachmentInfo.class); + when(pai.getPermission()).thenReturn("bskyblock.controlpanel.panel.vip"); + when(mockPlayer.getEffectivePermissions()).thenReturn(Set.of(pai)); + + ControlPanelObject result = manager.getUserControlPanel(user, world, "bskyblock."); + assertNotNull(result); + assertEquals("BSkyBlock_vip", result.getUniqueId()); + } + + @Test + void testSave() { + manager.save(); + } + + @Test + void testWipeDataEmptyGameMode() { + mockedUtils.when(() -> Utils.getGameMode(any(org.bukkit.World.class))).thenReturn(""); + User user = User.getInstance(mockPlayer); + manager.wipeData(world, user); + } + + @Test + void testWipeDataNoExistingPanels() { + User user = User.getInstance(mockPlayer); + manager.wipeData(world, user); + } + + @Test + void testImportControlPanelsNoFile() { + User user = User.getInstance(mockPlayer); + manager.importControlPanels(user, world, "nonexistent.yml"); + checkSpigotMessage("controlpanel.errors.no-file"); + } + + @Test + void testImportControlPanelsEmptyGameMode() { + mockedUtils.when(() -> Utils.getGameMode(any(org.bukkit.World.class))).thenReturn(""); + User user = User.getInstance(mockPlayer); + manager.importControlPanels(user, world, "test.yml"); + checkSpigotMessage("controlpanel.errors.not-a-gamemode-world"); + } + + @Test + void testImportControlPanelsNullUser() { + mockedUtils.when(() -> Utils.getGameMode(any(org.bukkit.World.class))).thenReturn(""); + manager.importControlPanels(null, world, "test.yml"); + } + + @Test + void testReload() { + manager.reload(); + } + + private ControlPanelObject createDefaultPanel() { + ControlPanelObject cpo = new ControlPanelObject(); + cpo.setUniqueId("BSkyBlock_default"); + cpo.setGameMode("BSkyBlock"); + cpo.setPanelName("Commands"); + cpo.setDefaultPanel(true); + + ControlPanelButton button = new ControlPanelButton(); + button.setSlot(0); + button.setName("Test"); + button.setCommand("test"); + button.setDescriptionLines(new ArrayList<>()); + cpo.setPanelButtons(new ArrayList<>(List.of(button))); + + return cpo; + } +} diff --git a/src/test/java/world/bentobox/controlpanel/panels/ControlPanelGeneratorTest.java b/src/test/java/world/bentobox/controlpanel/panels/ControlPanelGeneratorTest.java new file mode 100644 index 0000000..d076a82 --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/panels/ControlPanelGeneratorTest.java @@ -0,0 +1,160 @@ +package world.bentobox.controlpanel.panels; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; + +import world.bentobox.bentobox.api.panels.builders.PanelBuilder; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.controlpanel.CommonTestSetup; +import world.bentobox.controlpanel.ControlPanelAddon; +import world.bentobox.controlpanel.database.objects.ControlPanelObject; +import world.bentobox.controlpanel.database.objects.ControlPanelObject.ControlPanelButton; + +class ControlPanelGeneratorTest extends CommonTestSetup { + + @Mock + private ControlPanelAddon addon; + @Mock + private Server mockServer; + @Mock + private ConsoleCommandSender consoleSender; + + private ControlPanelObject controlPanel; + + @BeforeEach + @Override + public void setUp() throws Exception { + super.setUp(); + + when(addon.getPlugin()).thenReturn(plugin); + when(addon.getServer()).thenReturn(mockServer); + when(mockServer.getConsoleSender()).thenReturn(consoleSender); + + // Set up a control panel with buttons + controlPanel = new ControlPanelObject(); + controlPanel.setUniqueId("BSkyBlock_default"); + controlPanel.setGameMode("BSkyBlock"); + controlPanel.setPanelName("&1Commands"); + controlPanel.setDefaultPanel(true); + } + + @Test + void testOpenWithEmptyButtons() { + controlPanel.setPanelButtons(Collections.emptyList()); + User user = User.getInstance(mockPlayer); + + try (MockedConstruction mockedPB = Mockito.mockConstruction(PanelBuilder.class, + (mock, context) -> { + when(mock.user(any())).thenReturn(mock); + when(mock.name(anyString())).thenReturn(mock); + })) { + ControlPanelGenerator.open(addon, user, controlPanel, "island"); + PanelBuilder pb = mockedPB.constructed().get(0); + verify(pb).build(); + } + } + + @Test + void testOpenWithButton() { + ControlPanelButton button = new ControlPanelButton(); + button.setSlot(0); + button.setMaterial(Material.GRASS_BLOCK); + button.setName("Go Home"); + button.setCommand("island go"); + button.setDescriptionLines(List.of("Go to your island")); + + List buttons = new ArrayList<>(); + buttons.add(button); + controlPanel.setPanelButtons(buttons); + + User user = User.getInstance(mockPlayer); + + try (MockedConstruction mockedPB = Mockito.mockConstruction(PanelBuilder.class, + (mock, context) -> { + when(mock.user(any())).thenReturn(mock); + when(mock.name(anyString())).thenReturn(mock); + when(mock.item(any(int.class), any())).thenReturn(mock); + when(mock.item(any())).thenReturn(mock); + when(mock.slotOccupied(any(int.class))).thenReturn(false); + })) { + ControlPanelGenerator.open(addon, user, controlPanel, "island"); + PanelBuilder pb = mockedPB.constructed().get(0); + verify(pb).build(); + } + } + + @Test + void testOpenWithButtonWithIcon() { + ControlPanelButton button = new ControlPanelButton(); + button.setSlot(5); + button.setIcon(new ItemStack(Material.DIAMOND)); + button.setName("Special [label]"); + button.setCommand("[label] help"); + button.setDescriptionLines(List.of("Description for [player]")); + + List buttons = new ArrayList<>(); + buttons.add(button); + controlPanel.setPanelButtons(buttons); + + User user = User.getInstance(mockPlayer); + + try (MockedConstruction mockedPB = Mockito.mockConstruction(PanelBuilder.class, + (mock, context) -> { + when(mock.user(any())).thenReturn(mock); + when(mock.name(anyString())).thenReturn(mock); + when(mock.item(any(int.class), any())).thenReturn(mock); + when(mock.item(any())).thenReturn(mock); + when(mock.slotOccupied(any(int.class))).thenReturn(false); + })) { + ControlPanelGenerator.open(addon, user, controlPanel, "island"); + PanelBuilder pb = mockedPB.constructed().get(0); + verify(pb).build(); + } + } + + @Test + void testOpenWithInvalidSlot() { + ControlPanelButton button = new ControlPanelButton(); + button.setSlot(-1); // invalid slot + button.setMaterial(Material.STONE); + button.setName("Invalid"); + button.setCommand("test"); + button.setDescriptionLines(Collections.emptyList()); + + List buttons = new ArrayList<>(); + buttons.add(button); + controlPanel.setPanelButtons(buttons); + + User user = User.getInstance(mockPlayer); + + try (MockedConstruction mockedPB = Mockito.mockConstruction(PanelBuilder.class, + (mock, context) -> { + when(mock.user(any())).thenReturn(mock); + when(mock.name(anyString())).thenReturn(mock); + when(mock.item(any(int.class), any())).thenReturn(mock); + when(mock.item(any())).thenReturn(mock); + when(mock.slotOccupied(any(int.class))).thenReturn(false); + })) { + ControlPanelGenerator.open(addon, user, controlPanel, "island"); + PanelBuilder pb = mockedPB.constructed().get(0); + verify(pb).build(); + } + } +} diff --git a/src/test/java/world/bentobox/controlpanel/panels/GuiUtilsTest.java b/src/test/java/world/bentobox/controlpanel/panels/GuiUtilsTest.java new file mode 100644 index 0000000..28a978d --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/panels/GuiUtilsTest.java @@ -0,0 +1,183 @@ +package world.bentobox.controlpanel.panels; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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 java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.Test; + +import world.bentobox.controlpanel.CommonTestSetup; + +class GuiUtilsTest extends CommonTestSetup { + + @Test + void testStringSplitSimple() { + List result = GuiUtils.stringSplit("Hello world", 999, false); + assertFalse(result.isEmpty()); + assertEquals("Hello world", result.get(0)); + } + + @Test + void testStringSplitWithPipe() { + List result = GuiUtils.stringSplit("Line1|Line2|Line3", 999, false); + assertEquals(3, result.size()); + assertEquals("Line1", result.get(0)); + assertEquals("Line2", result.get(1)); + assertEquals("Line3", result.get(2)); + } + + @Test + void testStringSplitList() { + List input = Arrays.asList("Hello", "World"); + List result = GuiUtils.stringSplit(input, 999); + assertEquals(2, result.size()); + } + + @Test + void testStringSplitEmptyList() { + List input = Collections.emptyList(); + List result = GuiUtils.stringSplit(input, 999); + assertTrue(result.isEmpty()); + } + + @Test + void testGetMaterialItemNormal() { + ItemStack result = GuiUtils.getMaterialItem(Material.STONE); + assertNotNull(result); + assertEquals(Material.STONE, result.getType()); + assertEquals(1, result.getAmount()); + } + + @Test + void testGetMaterialItemWithAmount() { + ItemStack result = GuiUtils.getMaterialItem(Material.STONE, 5); + assertEquals(5, result.getAmount()); + } + + @Test + void testGetMaterialItemWallMaterial() { + ItemStack result = GuiUtils.getMaterialItem(Material.OAK_WALL_SIGN); + assertEquals(Material.OAK_SIGN, result.getType()); + } + + @Test + void testGetMaterialItemPottedPlant() { + ItemStack result = GuiUtils.getMaterialItem(Material.POTTED_OAK_SAPLING); + assertEquals(Material.OAK_SAPLING, result.getType()); + } + + @Test + void testGetMaterialItemMelonStem() { + ItemStack result = GuiUtils.getMaterialItem(Material.MELON_STEM); + assertEquals(Material.MELON_SEEDS, result.getType()); + } + + @Test + void testGetMaterialItemAttachedMelonStem() { + ItemStack result = GuiUtils.getMaterialItem(Material.ATTACHED_MELON_STEM); + assertEquals(Material.MELON_SEEDS, result.getType()); + } + + @Test + void testGetMaterialItemPumpkinStem() { + ItemStack result = GuiUtils.getMaterialItem(Material.PUMPKIN_STEM); + assertEquals(Material.PUMPKIN_SEEDS, result.getType()); + } + + @Test + void testGetMaterialItemCarrots() { + ItemStack result = GuiUtils.getMaterialItem(Material.CARROTS); + assertEquals(Material.CARROT, result.getType()); + } + + @Test + void testGetMaterialItemBeetroots() { + ItemStack result = GuiUtils.getMaterialItem(Material.BEETROOTS); + assertEquals(Material.BEETROOT, result.getType()); + } + + @Test + void testGetMaterialItemPotatoes() { + ItemStack result = GuiUtils.getMaterialItem(Material.POTATOES); + assertEquals(Material.POTATO, result.getType()); + } + + @Test + void testGetMaterialItemCocoa() { + ItemStack result = GuiUtils.getMaterialItem(Material.COCOA); + assertEquals(Material.COCOA_BEANS, result.getType()); + } + + @Test + void testGetMaterialItemKelpPlant() { + ItemStack result = GuiUtils.getMaterialItem(Material.KELP_PLANT); + assertEquals(Material.KELP, result.getType()); + } + + @Test + void testGetMaterialItemRedstoneWire() { + ItemStack result = GuiUtils.getMaterialItem(Material.REDSTONE_WIRE); + assertEquals(Material.REDSTONE, result.getType()); + } + + @Test + void testGetMaterialItemTripwire() { + ItemStack result = GuiUtils.getMaterialItem(Material.TRIPWIRE); + assertEquals(Material.STRING, result.getType()); + } + + @Test + void testGetMaterialItemFrostedIce() { + ItemStack result = GuiUtils.getMaterialItem(Material.FROSTED_ICE); + assertEquals(Material.ICE, result.getType()); + } + + @Test + void testGetMaterialItemEndPortal() { + ItemStack result = GuiUtils.getMaterialItem(Material.END_PORTAL); + assertEquals(Material.PAPER, result.getType()); + } + + @Test + void testGetMaterialItemWater() { + ItemStack result = GuiUtils.getMaterialItem(Material.WATER); + assertEquals(Material.WATER_BUCKET, result.getType()); + } + + @Test + void testGetMaterialItemLava() { + ItemStack result = GuiUtils.getMaterialItem(Material.LAVA); + assertEquals(Material.LAVA_BUCKET, result.getType()); + } + + @Test + void testGetMaterialItemFire() { + ItemStack result = GuiUtils.getMaterialItem(Material.FIRE); + assertEquals(Material.FIRE_CHARGE, result.getType()); + } + + @Test + void testGetMaterialItemAir() { + ItemStack result = GuiUtils.getMaterialItem(Material.AIR); + assertEquals(Material.GLASS_BOTTLE, result.getType()); + } + + @Test + void testGetMaterialItemPistonHead() { + ItemStack result = GuiUtils.getMaterialItem(Material.PISTON_HEAD); + assertEquals(Material.PISTON, result.getType()); + } + + @Test + void testWrapMethod() { + String result = GuiUtils.wrap("short", 100); + assertEquals("short", result); + } +} diff --git a/src/test/java/world/bentobox/controlpanel/utils/ConstantsTest.java b/src/test/java/world/bentobox/controlpanel/utils/ConstantsTest.java new file mode 100644 index 0000000..e8e4be6 --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/utils/ConstantsTest.java @@ -0,0 +1,69 @@ +package world.bentobox.controlpanel.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ConstantsTest { + + @Test + void testTitleConstant() { + assertEquals("controlpanel.gui.titles.", Constants.TITLE); + } + + @Test + void testButtonConstant() { + assertEquals("controlpanel.gui.buttons.", Constants.BUTTON); + } + + @Test + void testDescriptionConstant() { + assertEquals("controlpanel.gui.descriptions.", Constants.DESCRIPTION); + } + + @Test + void testMessageConstant() { + assertEquals("controlpanel.messages.", Constants.MESSAGE); + } + + @Test + void testErrorsConstant() { + assertEquals("controlpanel.errors.", Constants.ERRORS); + } + + @Test + void testQuestionsConstant() { + assertEquals("controlpanel.questions.", Constants.QUESTIONS); + } + + @Test + void testCommandsConstant() { + assertEquals("controlpanel.commands.", Constants.COMMANDS); + } + + @Test + void testTypesConstant() { + assertEquals("controlpanel.types.", Constants.TYPES); + } + + @Test + void testVariables() { + assertEquals("[gamemode]", Constants.VARIABLE_GAMEMODE); + assertEquals("[admin]", Constants.VARIABLE_ADMIN); + assertEquals("[file]", Constants.VARIABLE_FILENAME); + assertEquals("[message]", Constants.VARIABLE_MESSAGE); + } + + @Test + void testAllConstantsStartWithControlPanel() { + assertTrue(Constants.TITLE.startsWith("controlpanel.")); + assertTrue(Constants.BUTTON.startsWith("controlpanel.")); + assertTrue(Constants.DESCRIPTION.startsWith("controlpanel.")); + assertTrue(Constants.MESSAGE.startsWith("controlpanel.")); + assertTrue(Constants.ERRORS.startsWith("controlpanel.")); + assertTrue(Constants.QUESTIONS.startsWith("controlpanel.")); + assertTrue(Constants.COMMANDS.startsWith("controlpanel.")); + assertTrue(Constants.TYPES.startsWith("controlpanel.")); + } +} diff --git a/src/test/java/world/bentobox/controlpanel/utils/ItemsAdderParseTest.java b/src/test/java/world/bentobox/controlpanel/utils/ItemsAdderParseTest.java new file mode 100644 index 0000000..7357db8 --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/utils/ItemsAdderParseTest.java @@ -0,0 +1,28 @@ +package world.bentobox.controlpanel.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.Test; + +import world.bentobox.controlpanel.CommonTestSetup; + +class ItemsAdderParseTest extends CommonTestSetup { + + @Test + void testParseWhenItemsAdderNotEnabled() { + when(pim.isPluginEnabled("ItemsAdder")).thenReturn(false); + ItemStack defaultItem = new ItemStack(Material.STONE); + ItemStack result = ItemsAdderParse.parse("custom_item", defaultItem); + assertEquals(defaultItem, result); + } + + @Test + void testParseConvenienceMethodWhenNotEnabled() { + when(pim.isPluginEnabled("ItemsAdder")).thenReturn(false); + ItemStack result = ItemsAdderParse.parse("custom_item"); + assertEquals(Material.PAPER, result.getType()); + } +} diff --git a/src/test/java/world/bentobox/controlpanel/utils/UtilsTest.java b/src/test/java/world/bentobox/controlpanel/utils/UtilsTest.java new file mode 100644 index 0000000..7fab864 --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/utils/UtilsTest.java @@ -0,0 +1,155 @@ +package world.bentobox.controlpanel.utils; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.bukkit.World; +import org.bukkit.permissions.PermissionAttachmentInfo; +import org.junit.jupiter.api.Test; + +import world.bentobox.bentobox.api.addons.AddonDescription; +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.controlpanel.CommonTestSetup; + +class UtilsTest extends CommonTestSetup { + + @Test + void testGetPermissionValueWithMatch() { + User user = User.getInstance(mockPlayer); + PermissionAttachmentInfo pai = mock(PermissionAttachmentInfo.class); + when(pai.getPermission()).thenReturn("bskyblock.controlpanel.panel.vip"); + when(mockPlayer.getEffectivePermissions()).thenReturn(Set.of(pai)); + + String result = Utils.getPermissionValue(user, "bskyblock.controlpanel.panel", null); + assertEquals("vip", result); + } + + @Test + void testGetPermissionValueWithWildcard() { + User user = User.getInstance(mockPlayer); + PermissionAttachmentInfo pai = mock(PermissionAttachmentInfo.class); + when(pai.getPermission()).thenReturn("bskyblock.controlpanel.panel.*"); + when(mockPlayer.getEffectivePermissions()).thenReturn(Set.of(pai)); + + String result = Utils.getPermissionValue(user, "bskyblock.controlpanel.panel", "default"); + assertEquals("default", result); + } + + @Test + void testGetPermissionValueNoMatch() { + User user = User.getInstance(mockPlayer); + when(mockPlayer.getEffectivePermissions()).thenReturn(Collections.emptySet()); + + String result = Utils.getPermissionValue(user, "bskyblock.controlpanel.panel", "fallback"); + assertEquals("fallback", result); + } + + @Test + void testGetPermissionValueTrailingDot() { + User user = User.getInstance(mockPlayer); + PermissionAttachmentInfo pai = mock(PermissionAttachmentInfo.class); + when(pai.getPermission()).thenReturn("bskyblock.controlpanel.panel.admin"); + when(mockPlayer.getEffectivePermissions()).thenReturn(Set.of(pai)); + + String result = Utils.getPermissionValue(user, "bskyblock.controlpanel.panel.", null); + assertEquals("admin", result); + } + + @Test + void testGetGameModeFromWorld() { + GameModeAddon gma = mock(GameModeAddon.class); + AddonDescription desc = new AddonDescription.Builder("main", "BSkyBlock", "1.0").build(); + when(gma.getDescription()).thenReturn(desc); + when(iwm.getAddon(any(World.class))).thenReturn(Optional.of(gma)); + + String result = Utils.getGameMode(world); + assertEquals("BSkyBlock", result); + } + + @Test + void testGetGameModeFromWorldNoAddon() { + when(iwm.getAddon(any(World.class))).thenReturn(Optional.empty()); + + String result = Utils.getGameMode(world); + assertEquals("", result); + } + + @Test + void testGetGameModeFromAddon() { + GameModeAddon gma = mock(GameModeAddon.class); + AddonDescription desc = new AddonDescription.Builder("main", "AcidIsland", "1.0").build(); + when(gma.getDescription()).thenReturn(desc); + + String result = Utils.getGameMode(gma); + assertEquals("AcidIsland", result); + } + + @Test + void testGetNextValue() { + String[] values = {"a", "b", "c"}; + assertEquals("b", Utils.getNextValue(values, "a")); + assertEquals("c", Utils.getNextValue(values, "b")); + assertEquals("a", Utils.getNextValue(values, "c")); // wrap around + } + + @Test + void testGetNextValueNotFound() { + String[] values = {"a", "b", "c"}; + assertEquals("d", Utils.getNextValue(values, "d")); // returns current if not found + } + + @Test + void testGetPreviousValue() { + String[] values = {"a", "b", "c"}; + assertEquals("c", Utils.getPreviousValue(values, "a")); // wrap around + assertEquals("a", Utils.getPreviousValue(values, "b")); + assertEquals("b", Utils.getPreviousValue(values, "c")); + } + + @Test + void testGetPreviousValueNotFound() { + String[] values = {"a", "b", "c"}; + assertEquals("d", Utils.getPreviousValue(values, "d")); // returns current if not found + } + + @Test + void testReadIntArrayWithIntegers() { + int[] result = Utils.readIntArray(List.of(1, 3, 5)); + assertArrayEquals(new int[]{1, 3, 5}, result); + } + + @Test + void testReadIntArrayWithRange() { + int[] result = Utils.readIntArray(List.of("1-3")); + assertArrayEquals(new int[]{1, 2, 3}, result); + } + + @Test + void testReadIntArrayWithMixed() { + int[] result = Utils.readIntArray(List.of(0, "2-4", 7)); + assertArrayEquals(new int[]{0, 2, 3, 4, 7}, result); + } + + @Test + void testReadIntArrayEmpty() { + int[] result = Utils.readIntArray(Collections.emptyList()); + assertArrayEquals(new int[]{}, result); + } + + @Test + void testReadIntArraySingleStringNumber() { + int[] result = Utils.readIntArray(List.of("5")); + assertArrayEquals(new int[]{5}, result); + } +}