diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e99f9f3..adeea35 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,22 +10,22 @@ jobs: name: Build and analyze runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up JDK 17 - uses: actions/setup-java@v3 + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - java-version: 17 + java-version: 21 distribution: 'zulu' # Alternative distribution options are available. - name: Cache SonarCloud packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Maven packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} @@ -34,4 +34,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=BentoBoxWorld_ControlPanel \ No newline at end of file + run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=BentoBoxWorld_ControlPanel 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/README.md b/README.md index 5294fdd..2a3cd8f 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,405 @@ -# ControlPanel Addon +# 🎛️ ControlPanel Addon for BentoBox + [![Discord](https://img.shields.io/discord/272499714048524288.svg?logo=discord)](https://discord.bentobox.world) [![Build Status](https://ci.codemc.org/buildStatus/icon?job=BentoBoxWorld/ControlPanel)](https://ci.codemc.org/job/BentoBoxWorld/job/ControlPanel/) -This is simple ControlPanel for all BentoBox GameMode addons. Allows to customize GUI for users. +Give your players a slick, clickable GUI menu to run their most-used island commands — no typing required. ControlPanel is a [BentoBox](https://github.com/BentoBoxWorld/BentoBox) addon that lets server admins build fully customizable control panels using a simple YAML file. + +--- + +## ✨ Features + +- 🖱️ **Click-to-run** — left-click, right-click, and shift-click can each trigger a different command +- 🗂️ **Multiple panels** — define as many panels as you want per gamemode; assign them to players via permissions +- 🎨 **Rich icons** — use any vanilla material, BentoBox `ItemParser` format, or [ItemsAdder](https://github.com/LoneDev6/ItemsAdder) custom items +- 📋 **Live placeholders** — button descriptions support PlaceholderAPI, color codes, and `[gamemode]` substitution +- 📦 **Console commands** — prefix a command with `[server]` to run it as the console +- 🔢 **Slot ranges** — fill a row of slots with a single button definition using `"0-8"` +- 🌍 **13 localisations** — cs, de, en-US, es, fr, id, ko, lv, pl, ru, zh-CN, zh-HK, zh-TW +- 🔌 **Works with** AcidIsland, BSkyBlock, CaveBlock, SkyGrid, AOneBlock, etc. + +--- + +## 📦 Installation + +1. Drop the `ControlPanel` JAR into `plugins/BentoBox/addons/`. +2. Restart the server — ControlPanel creates a default `controlPanelTemplate.yml` in `plugins/BentoBox/addons/ControlPanel/`. +3. Edit `controlPanelTemplate.yml` to build your panels (see below). +4. Run the import command in-game to load your panels: + ``` + /bsb cp import + ``` + Supply a custom filename to import from a different file: + ``` + /bsb cp import myCustomPanels + ``` + > ⚠️ Importing when panels already exist will ask for confirmation and replace them. + +--- + +## 🎮 Commands + +### Player command +``` +/is cp +/is controlpanel +``` +Opens the player's assigned control panel. Players without a specific panel permission see the `defaultPanel`. + +### Admin command +``` +/{admin} cp import [filename] +/{admin} cp import +``` +| Command | Description | +|---|---| +| `/{admin} cp import` | Import panels from `controlPanelTemplate.yml` (default) | +| `/{admin} cp import myFile` | Import panels from `myFile.yml` in the addon data folder | + +> Replace `{admin}` with your gamemode's admin command, e.g. `bsb` for BSkyBlock or `oa` for AOneBlock. + +--- + +## 🔑 Permissions + +| Permission | Default | Description | +|---|---|---| +| `[gamemode].controlpanel` | `true` | Open the control panel | +| `[gamemode].controlpanel.admin` | `op` | Use admin import command | +| `[gamemode].controlpanel.panel.default` | `true` | Access the default panel | +| `[gamemode].controlpanel.panel.` | — | Access a custom panel named `` | + +Replace `[gamemode]` with the gamemode name in lowercase, e.g. `bskyblock`, `acidisland`, `aoneblock`. + +--- + +## 🛠️ Configuration File Reference + +The template file lives at `plugins/BentoBox/addons/ControlPanel/controlPanelTemplate.yml`. + +### Top-level structure + +```yaml +panel-list: + : + defaultPanel: true|false + panelName: '' + permission: '<suffix>' + buttons: + <slot>: + name: '<display name>' + material: MATERIAL_NAME + icon: 'namespace:item_id' + itemsadder: 'namespace:item_id' + description: |- + line one + line two + command: '<left-click command>' + right_click_command: '<right-click command>' + shift_click_command: '<shift+left-click command>' +``` + +### Panel fields + +| Field | Description | +|---|---| +| `defaultPanel` | `true` = shown to players with no specific panel permission | +| `panelName` | Title of the inventory GUI. Supports `&` color codes. | +| `permission` | Suffix for `[gamemode].controlpanel.panel.<suffix>`. Players with that permission see this panel. | + +### Button fields + +| Field | Description | +|---|---| +| `slot` | Inventory slot (0–53). Also accepts a range like `"0-8"` to fill multiple slots. | +| `name` | Button display name. Supports `&` color codes and command placeholders. | +| `material` | Vanilla Minecraft material (e.g. `GRASS_BLOCK`). Resolved with `Material.match`. | +| `icon` | BentoBox `ItemParser` string (e.g. `minecraft:diamond`). **Takes priority over `material`.** | +| `itemsadder` | ItemsAdder custom item ID (e.g. `iasurvival:ruby`). Requires the ItemsAdder plugin. | +| `description` | Lore lines shown under the button name. Supports `&` color codes, multi-line `\|`, PlaceholderAPI `%placeholders%`, and `[gamemode]`. | +| `command` | Command run on **left-click** (and any click with no specific override). Supports placeholders. | +| `right_click_command` | Command run on **right-click** or **shift+right-click**. Falls back to `command` if omitted. | +| `shift_click_command` | Command run on **shift+left-click**. Falls back to `command` if omitted. | + +### Click type summary + +| Click | Command used | +|---|---| +| Left click | `command` | +| Right click | `right_click_command` → `command` | +| Shift + Right click | `right_click_command` → `command` | +| Shift + Left click | `shift_click_command` → `command` | +| Any other click | `command` | + +### Command placeholders + +| Placeholder | Replaced with | +|---|---| +| `[label]` | The gamemode's player command label (e.g. `island`, `ob`) | +| `[player]` | The clicking player's username | +| `[server]` | Runs the command as the server console instead of the player | + +### Description placeholders + +| Placeholder | Replaced with | +|---|---| +| `[gamemode]` | Lowercase gamemode name (e.g. `bskyblock`, `aoneblock`) | +| `%PlaceholderAPI_placeholder%` | Any registered PlaceholderAPI placeholder | + +--- + +## 📄 Example Configuration + +A complete, real-world example for an active **BSkyBlock / AOneBlock** server. It shows two panels — a default panel for all players and a VIP panel for donors — along with all button features. + +```yaml +panel-list: + + # ── Default panel — shown to all players ────────────────────────────────── + default: + defaultPanel: true + panelName: '&8&l⚙ &r&6&l Island Control Panel' + permission: 'default' + buttons: + + # Row 0: decorative glass pane border + "0-8": + name: ' ' + material: BLACK_STAINED_GLASS_PANE + description: '' + command: '' + + # ── Navigation ──────────────────────────────────────────────────────── + 9: + name: '&a&l 🏝 Go to Island' + icon: minecraft:grass_block + description: |- + &7 Left-click: teleport to your island. + &7 Right-click: go to your island nether. + &7 Shift+click: set your home here. + &7 Island level: &e%Level_[gamemode]_island_level% + command: '[label] go' + right_click_command: '[label] go nether' + shift_click_command: '[label] sethome' + + 10: + name: '&e&l 🏠 Set Home' + icon: minecraft:white_bed + description: |- + &7 Set your island home + &7 to your current location. + command: '[label] sethome' + + 11: + name: '&b&l 👥 Team' + icon: minecraft:player_head + description: |- + &7 View and manage + &7 your island team. + command: '[label] team' + + 12: + name: '&d&l ✉ Invite' + icon: minecraft:paper + description: |- + &7 Invite a player to + &7 your island. + &7 Usage: &f/[label] invite <player> + command: '[label] invite' + + 13: + name: '&6&l ⚙ Settings' + icon: minecraft:anvil + description: |- + &7 Configure your island + &7 protection settings. + command: '[label] settings' + + # ── Stats & Leaderboards ────────────────────────────────────────────── + 18: + name: '&e&l ⭐ Island Level' + icon: minecraft:experience_bottle + description: |- + &7 Calculate your island level. + &7 Current level: &e%Level_[gamemode]_island_level% + &7 Current rank: &6%Level_[gamemode]_island_rank% + command: '[label] level' + + 19: + name: '&6&l 🏆 Top Islands' + icon: minecraft:gold_block + description: |- + &7 View the top 10 islands + &7 on this server. + command: '[label] top' + + 20: + name: '&a&l 🌿 Warps' + icon: minecraft:ender_eye + description: |- + &7 Browse player warps. + command: '[label] warps' + + 21: + name: '&5&l 🔮 Challenges' + icon: minecraft:enchanting_table + description: |- + &7 Complete challenges + &7 for great rewards! + command: '[label] challenges' + + 22: + name: '&b&l 🌊 Biomes' + icon: minecraft:oak_sapling + description: |- + &7 Change the biome + &7 on your island. + command: '[label] biomes' + + # ── World / Utility ─────────────────────────────────────────────────── + 27: + name: '&f&l 🌍 Spawn' + icon: minecraft:bedrock + description: |- + &7 Teleport to world spawn. + command: '[label] spawn' + + 28: + name: '&c&l 🛒 Shop' + icon: minecraft:emerald + description: |- + &7 Open the server shop. + command: 'shop' + + 29: + name: '&e&l 💰 Balance' + icon: minecraft:gold_nugget + description: |- + &7 Check your current balance. + &7 Balance: &6%vault_balance% + command: 'balance' + + # Console command example — runs as server, not player + 35: + name: '&c&l 🔔 Report Bug' + icon: minecraft:writable_book + description: |- + &7 Report an issue to admins. + &7 Opens a support ticket. + command: '[server] ticket create [player] bug-report' + + # Row 5: decorative border + "36-44": + name: ' ' + material: BLACK_STAINED_GLASS_PANE + description: '' + command: '' + + # ── VIP panel — assigned via permission bskyblock.controlpanel.panel.vip ─ + vip: + defaultPanel: false + panelName: '&8&l⚙ &r&d&l VIP Control Panel' + permission: 'vip' + buttons: + + "0-8": + name: ' ' + material: PURPLE_STAINED_GLASS_PANE + description: '' + command: '' + + 9: + name: '&a&l 🏝 Go to Island' + icon: minecraft:grass_block + description: |- + &7 Teleport to your island. + &7 Island level: &e%Level_[gamemode]_island_level% + command: '[label] go' + + 10: + name: '&e&l 🏠 Set Home' + icon: minecraft:white_bed + description: |- + &7 Set your island home. + command: '[label] sethome' + + 11: + name: '&b&l 👥 Team' + icon: minecraft:player_head + description: |- + &7 View and manage your team. + command: '[label] team' + + 12: + name: '&5&l 🔮 Challenges' + icon: minecraft:enchanting_table + description: |- + &7 Complete challenges! + command: '[label] challenges' + + # VIP exclusive — grants a kit via console + 13: + name: '&d&l 🎁 VIP Kit' + icon: minecraft:chest + description: |- + &d VIP exclusive! + &7 Claim your weekly VIP kit. + command: '[server] kit vipweekly [player]' + + # VIP exclusive — ItemsAdder custom icon example + 14: + name: '&6&l ✨ VIP Perks' + itemsadder: 'iasurvival:vip_star' + description: |- + &7 Browse all your VIP perks. + command: 'vipperks' + + 18: + name: '&e&l ⭐ Island Level' + icon: minecraft:experience_bottle + description: |- + &7 Current level: &e%Level_[gamemode]_island_level% + &7 Current rank: &6%Level_[gamemode]_island_rank% + command: '[label] level' + + 19: + name: '&6&l 🏆 Top Islands' + icon: minecraft:gold_block + description: |- + &7 View the top 10 islands. + command: '[label] top' + + 20: + name: '&f&l 🌍 Spawn' + icon: minecraft:bedrock + description: |- + &7 Teleport to world spawn. + command: '[label] spawn' + + "36-44": + name: ' ' + material: PURPLE_STAINED_GLASS_PANE + description: '' + command: '' +``` + +--- -## How to use +## 💡 Tips -1. Place the addon jar in the addons folder of the BentoBox plugin -2. Restart the server -3. Use admin command to import control panels. +- **Alternative clicks** — add `right_click_command` and/or `shift_click_command` to a button to run different commands on right-click and shift+left-click. Omitting them falls back to `command`. +- **Slot layout** — a standard chest row is slots 0–8. A 6-row chest (the max) uses slots 0–53. +- **Slot ranges** — use `"0-8"` (quoted) to place the same button across a range of slots. Great for border decorations. +- **Blank buttons** — set `command: ''` and `name: ' '` to create a visual spacer or border. +- **Console commands** — prefix `[server]` to run a command silently as the console, e.g. for kits, economy rewards, or admin actions players wouldn't have permission to run directly. +- **Multiple panels** — give a player the permission `[gamemode].controlpanel.panel.<suffix>` to show them a different panel. Handy for staff, donors, or ranked players. +- **ItemsAdder icons** — use the `itemsadder:` key (instead of `icon:` or `material:`) to display a custom-textured item. Requires ItemsAdder to be installed; falls back gracefully if it isn't. +- **Reloading** — after editing the YAML, run `/{admin} cp import` again to reload. Existing panels will be replaced after confirmation. -## Compatibility +--- -- [x] BentoBox - 1.7.0 version -- [x] BSkyBlock -- [x] AcidIsland -- [x] SkyGrid -- [x] CaveBlock +## 🌐 Information -## Information +More information can be found in the [BentoBox Docs](https://docs.bentobox.world/en/latest/addons/ControlPanel/). -More information can be found in [Wiki Pages](https://github.com/BentoBoxWorld/ControlPanel/wiki). +Need help? Join the [BentoBox Discord](https://discord.bentobox.world). diff --git a/pom.xml b/pom.xml index 6f6f289..a034c5e 100644 --- a/pom.xml +++ b/pom.xml @@ -52,23 +52,26 @@ <!-- Some JAVA encoding settings --> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> - <java.version>17</java.version> + <java.version>21</java.version> - <!-- SPIGOT API version --> - <spigot.version>1.21.3-R0.1-SNAPSHOT</spigot.version> + <!-- PAPER API version --> + <paper.version>1.21.11-R0.1-SNAPSHOT</paper.version> <!-- BentoBox API version --> - <bentobox.version>2.7.1-SNAPSHOT</bentobox.version> + <bentobox.version>3.10.0</bentobox.version> <!-- Revision variable removes warning about dynamic version --> <revision>${build.version}-SNAPSHOT</revision> <!-- This allows to change between versions and snapshots. --> - <build.version>1.14.0</build.version> + <build.version>1.16.0</build.version> <build.number>-LOCAL</build.number> <!-- Sonar Cloud --> <sonar.projectKey>BentoBoxWorld_ControlPanel</sonar.projectKey> <sonar.organization>bentobox-world</sonar.organization> <sonar.host.url>https://sonarcloud.io</sonar.host.url> + <!-- Test dependency versions --> + <junit.version>5.10.2</junit.version> + <mockito.version>5.11.0</mockito.version> </properties> <profiles> @@ -104,13 +107,17 @@ <!-- Repositories contains links from were dependencies will be searched --> <repositories> + <!-- jitpack first so MockBukkit snapshots resolve without hitting other repos --> <repository> - <id>spigot-repo</id> - <url>https://hub.spigotmc.org/nexus/content/repositories/snapshots</url> + <id>jitpack.io</id> + <url>https://jitpack.io</url> + <snapshots> + <enabled>true</enabled> + </snapshots> </repository> <repository> - <id>spigotmc-public</id> - <url>https://hub.spigotmc.org/nexus/content/groups/public/</url> + <id>papermc</id> + <url>https://repo.papermc.io/repository/maven-public/</url> </repository> <repository> <id>bentoboxworld</id> @@ -124,14 +131,48 @@ <id>vault-repo</id> <url>http://nexus.hc.to/content/repositories/pub_releases</url> </repository> + <repository> + <id>matteodev</id> + <url>https://maven.devs.beer/</url> + </repository> </repositories> - <!-- Your addon must contain Spigot and BentoBox APIs dependencies. --> + <!-- Your addon must contain Paper and BentoBox APIs dependencies. --> <dependencies> <dependency> - <groupId>org.spigotmc</groupId> - <artifactId>spigot-api</artifactId> - <version>${spigot.version}</version> + <groupId>com.github.MockBukkit</groupId> + <artifactId>MockBukkit</artifactId> + <version>v1.21-SNAPSHOT</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <version>${junit.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>${junit.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-junit-jupiter</artifactId> + <version>${mockito.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>${mockito.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.papermc.paper</groupId> + <artifactId>paper-api</artifactId> + <version>${paper.version}</version> <scope>provided</scope> </dependency> @@ -147,6 +188,13 @@ <artifactId>annotations</artifactId> <version>18.0.0</version> </dependency> + + <dependency> + <groupId>dev.lone</groupId> + <artifactId>api-itemsadder</artifactId> + <version>4.0.10</version> + <scope>provided</scope> + </dependency> </dependencies> <!-- Build contains information for maven. It allows to create correct @@ -214,11 +262,12 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> - <version>3.1.2</version> + <version>3.5.4</version> <!--suppress MavenModelInspection --> <configuration> <argLine> ${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 @@ -300,7 +349,7 @@ <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> - <version>0.8.10</version> + <version>0.8.13</version> <configuration> <append>true</append> <excludes> diff --git a/src/main/java/world/bentobox/controlpanel/commands/user/PlayerCommand.java b/src/main/java/world/bentobox/controlpanel/commands/user/PlayerCommand.java index 533a10c..0739ed8 100644 --- a/src/main/java/world/bentobox/controlpanel/commands/user/PlayerCommand.java +++ b/src/main/java/world/bentobox/controlpanel/commands/user/PlayerCommand.java @@ -3,8 +3,10 @@ import java.util.List; +import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.util.Util; import world.bentobox.controlpanel.ControlPanelAddon; import world.bentobox.controlpanel.database.objects.ControlPanelObject; import world.bentobox.controlpanel.panels.ControlPanelGenerator; @@ -70,9 +72,11 @@ public boolean canExecute(User user, String label, List<String> args) { if (user.isOp()) { - user.sendMessage(Constants.ERRORS + "no-valid-panels-op", - Constants.VARIABLE_ADMIN, - this.getTopLabel()); + this.getIWM().getAddon(this.getWorld()). + flatMap(GameModeAddon::getAdminCommand). + ifPresent(command -> user.sendMessage(Constants.ERRORS + "no-valid-panels-op", + Constants.VARIABLE_ADMIN, + command.getTopLabel())); } else { diff --git a/src/main/java/world/bentobox/controlpanel/database/objects/ControlPanelObject.java b/src/main/java/world/bentobox/controlpanel/database/objects/ControlPanelObject.java index c56079b..08fc904 100644 --- a/src/main/java/world/bentobox/controlpanel/database/objects/ControlPanelObject.java +++ b/src/main/java/world/bentobox/controlpanel/database/objects/ControlPanelObject.java @@ -10,6 +10,7 @@ import java.util.List; import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; import com.google.gson.annotations.Expose; @@ -235,6 +236,28 @@ public void setMaterial(Material material) } + /** + * Gets icon. + * + * @return the icon + */ + public ItemStack getIcon() + { + return icon; + } + + + /** + * Sets icon. + * + * @param icon the icon + */ + public void setIcon(ItemStack icon) + { + this.icon = icon; + } + + /** * Method ControlPanelButton#getDescription returns the description of this object. * @@ -281,6 +304,48 @@ public void setCommand(String command) } + /** + * Method ControlPanelButton#getRightClickCommand returns the rightClickCommand of this object. + * + * @return the rightClickCommand (type String) of this object. + */ + public String getRightClickCommand() + { + return rightClickCommand; + } + + + /** + * Method ControlPanelButton#setRightClickCommand sets new value for the rightClickCommand of this object. + * @param rightClickCommand new value for this object. + */ + public void setRightClickCommand(String rightClickCommand) + { + this.rightClickCommand = rightClickCommand; + } + + + /** + * Method ControlPanelButton#getShiftClickCommand returns the shiftClickCommand of this object. + * + * @return the shiftClickCommand (type String) of this object. + */ + public String getShiftClickCommand() + { + return shiftClickCommand; + } + + + /** + * Method ControlPanelButton#setShiftClickCommand sets new value for the shiftClickCommand of this object. + * @param shiftClickCommand new value for this object. + */ + public void setShiftClickCommand(String shiftClickCommand) + { + this.shiftClickCommand = shiftClickCommand; + } + + /** * Method ControlPanelButton#getName returns the name of this object. * @@ -337,8 +402,15 @@ public void setDescriptionLines(List<String> descriptionLines) * Material icon for button */ @Expose + @Deprecated private Material material; + /** + * ItemStack for icon + */ + @Expose + private ItemStack icon; + /** * Description for the button */ @@ -358,6 +430,18 @@ public void setDescriptionLines(List<String> descriptionLines) @Expose private String command; + /** + * Command that will run on right click. + */ + @Expose + private String rightClickCommand; + + /** + * Command that will run on shift+left click. + */ + @Expose + private String shiftClickCommand; + /** * Name of the Button. */ diff --git a/src/main/java/world/bentobox/controlpanel/managers/ControlPanelManager.java b/src/main/java/world/bentobox/controlpanel/managers/ControlPanelManager.java index 3b619f0..e7cc2f0 100644 --- a/src/main/java/world/bentobox/controlpanel/managers/ControlPanelManager.java +++ b/src/main/java/world/bentobox/controlpanel/managers/ControlPanelManager.java @@ -20,17 +20,20 @@ import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.util.ItemParser; import world.bentobox.controlpanel.ControlPanelAddon; import world.bentobox.controlpanel.database.objects.ControlPanelObject; import world.bentobox.controlpanel.database.objects.ControlPanelObject.ControlPanelButton; import world.bentobox.controlpanel.panels.GuiUtils; import world.bentobox.controlpanel.utils.Constants; +import world.bentobox.controlpanel.utils.ItemsAdderParse; import world.bentobox.controlpanel.utils.Utils; @@ -245,7 +248,6 @@ public void importControlPanels(@Nullable User user, @NotNull GameModeAddon addo * @param user - user * @param gameModeName - gamemode name where ControlPanels must be imported. * @param fileName Specifies from which file control panel will be loaded - * @return true if successful */ private void importControlPanels(@Nullable User user, String gameModeName, @NotNull String fileName) { @@ -354,47 +356,55 @@ private void readControlPanel(YamlConfiguration config, @Nullable User user, fin if (buttonListSection != null) { buttonListSection.getKeys(false).forEach(slotReference -> { - ControlPanelButton button = new ControlPanelButton(); - button.setSlot(Integer.parseInt(slotReference)); + for (int slotNum : Utils.readIntArray(List.of(slotReference))) { + ControlPanelButton button = new ControlPanelButton(); + button.setSlot(slotNum); - ConfigurationSection buttonSection = - buttonListSection.getConfigurationSection(slotReference); + ConfigurationSection buttonSection = + buttonListSection.getConfigurationSection(slotReference); - if (buttonSection != null) - { - button.setName(buttonSection.getString("name")); - button.setCommand(buttonSection.getString("command", "[user_command]")); - - // Create empty list - button.setDescriptionLines(new ArrayList<>()); - - if (buttonSection.isList("description")) - { - // Read description by each line - buttonSection.getStringList("description").forEach(line -> - button.getDescriptionLines().add( - line.replace("[gamemode]", gameMode.toLowerCase()))); - } - else if (buttonSection.isString("description")) + if (buttonSection != null) { - // Check if description is not defined as simple string - String input = buttonSection.getString("description", ""); + button.setName(buttonSection.getString("name")); + button.setCommand(buttonSection.getString("command", "[user_command]")); + button.setRightClickCommand(buttonSection.getString("right_click_command")); + button.setShiftClickCommand(buttonSection.getString("shift_click_command")); - if (input != null && !input.isEmpty()) + // Create empty list + button.setDescriptionLines(new ArrayList<>()); + + if (buttonSection.isList("description")) { - button.getDescriptionLines().add( - input.replace("[gamemode]", gameMode.toLowerCase())); + // Read description by each line + buttonSection.getStringList("description").forEach(line -> + button.getDescriptionLines().add( + line.replace("[gamemode]", gameMode.toLowerCase()))); + } + else if (buttonSection.isString("description")) + { + // Check if description is not defined as simple string + String input = buttonSection.getString("description", ""); + + if (!input.isEmpty()) + { + button.getDescriptionLines().add( + input.replace("[gamemode]", gameMode.toLowerCase())); + } + } + else + { + this.addon.logWarning("Description for button " + + button.getSlot() + " could not be read."); } - } - else - { - this.addon.logWarning("Description for button " + - + button.getSlot() + " could not be read."); - } - button.setMaterial(Material.matchMaterial(buttonSection.getString("material", "GRASS"))); + button.setMaterial(Material.matchMaterial(buttonSection.getString("material", "GRASS"))); + if(buttonSection.getString("icon") != null) + button.setIcon(ItemParser.parse(buttonSection.getString("icon"), new ItemStack(Material.PAPER))); + if(buttonSection.getString("itemsadder") != null) + button.setIcon(ItemsAdderParse.parse(buttonSection.getString("itemsadder"))); - buttonList.add(button); + buttonList.add(button); + } } }); } @@ -498,15 +508,15 @@ public ControlPanelObject getUserControlPanel(User user, World world, String per /** * Control Panel Addon instance. */ - private ControlPanelAddon addon; + private final ControlPanelAddon addon; /** * This database allows to access to all stored control panels. */ - private Database<ControlPanelObject> controlPanelDatabase; + private final Database<ControlPanelObject> controlPanelDatabase; /** * This map contains all control panel object linked to their reference game mode. */ - private Map<String, ControlPanelObject> controlPanelCache; + private final Map<String, ControlPanelObject> controlPanelCache; } diff --git a/src/main/java/world/bentobox/controlpanel/panels/ControlPanelGenerator.java b/src/main/java/world/bentobox/controlpanel/panels/ControlPanelGenerator.java index 688a00f..2e0890c 100644 --- a/src/main/java/world/bentobox/controlpanel/panels/ControlPanelGenerator.java +++ b/src/main/java/world/bentobox/controlpanel/panels/ControlPanelGenerator.java @@ -8,6 +8,8 @@ import org.bukkit.Material; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -116,10 +118,17 @@ private PanelItem generateButton(ControlPanelButton button) return new PanelItemBuilder(). name(buttonName). - icon(button.getMaterial() == null ? Material.PAPER : button.getMaterial()). + icon(button.getIcon() == null ? new ItemStack(button.getMaterial() == null ? Material.PAPER : button.getMaterial()) : button.getIcon()). description(description). clickHandler((panel, user, clickType, slot) -> { - final String parsedCommand = button.getCommand(). + final String rawCommand = this.getCommandForClickType(button, clickType); + + if (rawCommand == null || rawCommand.isEmpty()) + { + return true; + } + + final String parsedCommand = rawCommand. replace("[label]", this.topLabel). replace("[server]", ""). replace("[player]", user.getName()). @@ -127,7 +136,7 @@ private PanelItem generateButton(ControlPanelButton button) if (!parsedCommand.isEmpty()) { - if (button.getCommand().startsWith("[server]")) + if (rawCommand.startsWith("[server]")) { if (!this.addon.getServer().dispatchCommand( this.addon.getServer().getConsoleSender(), @@ -153,6 +162,42 @@ private PanelItem generateButton(ControlPanelButton button) } + /** + * This method returns the command string for a given click type. + * If a specific click type command is not defined, falls back to the default command. + * @param button ControlPanelButton that contains command definitions. + * @param clickType The type of click performed. + * @return The command string to execute, or null if no command is defined. + */ + private String getCommandForClickType(ControlPanelButton button, ClickType clickType) + { + switch (clickType) + { + case RIGHT: + case SHIFT_RIGHT: + if (button.getRightClickCommand() != null && !button.getRightClickCommand().isEmpty()) + { + return button.getRightClickCommand(); + } + + break; + + case SHIFT_LEFT: + if (button.getShiftClickCommand() != null && !button.getShiftClickCommand().isEmpty()) + { + return button.getShiftClickCommand(); + } + + break; + + default: + break; + } + + return button.getCommand(); + } + + // --------------------------------------------------------------------- // Section: Variables // --------------------------------------------------------------------- diff --git a/src/main/java/world/bentobox/controlpanel/utils/ItemsAdderParse.java b/src/main/java/world/bentobox/controlpanel/utils/ItemsAdderParse.java new file mode 100644 index 0000000..3af5446 --- /dev/null +++ b/src/main/java/world/bentobox/controlpanel/utils/ItemsAdderParse.java @@ -0,0 +1,20 @@ +package world.bentobox.controlpanel.utils; + +import dev.lone.itemsadder.api.CustomStack; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +public class ItemsAdderParse { + public static ItemStack parse(String string, ItemStack itemStack) + { + if(!Bukkit.getServer().getPluginManager().isPluginEnabled("ItemsAdder")) return itemStack; + CustomStack stack = CustomStack.getInstance(string); + if(stack != null) return stack.getItemStack(); + return itemStack; + } + public static ItemStack parse(String string) + { + return parse(string, new ItemStack(Material.PAPER)); + } +} diff --git a/src/main/java/world/bentobox/controlpanel/utils/Utils.java b/src/main/java/world/bentobox/controlpanel/utils/Utils.java index 11b4276..ded5ac7 100644 --- a/src/main/java/world/bentobox/controlpanel/utils/Utils.java +++ b/src/main/java/world/bentobox/controlpanel/utils/Utils.java @@ -9,8 +9,11 @@ import org.bukkit.World; import org.bukkit.permissions.PermissionAttachmentInfo; + +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.IntStream; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.GameModeAddon; @@ -143,4 +146,61 @@ public static <T> T getPreviousValue(T[] values, T currentValue) return currentValue; } + + /** + * Reads a list of objects and converts it into an array of integers. + * <p> + * The method processes each element in the list: + * <ul> + * <li>If the element is an Integer, it adds it directly to the result array.</li> + * <li>If the element is a String in the format "start-end", it adds all integers + * from "start" to "end" (inclusive) to the result array.</li> + * </ul> + * + * <b>Example:</b> + * If the input list is `["1-3", 5, "7-9"]`, the method will return an array + * containing `[1, 2, 3, 5, 7, 8, 9]`. + * + * @param objectList A list of objects, each of which can either be an Integer or a String + * in the format "start-end" (inclusive range). + * @return An array of integers containing all the integers parsed from the input list. + * The integers from ranges are added to the result array in order. + * @throws NumberFormatException If the string representation of a number is not valid. + */ + public static int[] readIntArray(List<?> objectList) { + List<Integer> values = new ArrayList<>(); + + // Process each item in the objectList + for (Object o : objectList) { + // If the object is an Integer, add it directly to values + if (o instanceof Integer) { + values.add((int) o); + } + // If the object is a String in the form "start-end" + else if (o instanceof String) { + try { + int n = Integer.parseInt((String) o); + values.add(n); + }catch (NumberFormatException ignored) {} + String[] args = ((String) o).split("-"); + if (args.length >= 2) { + try { + int n0 = Integer.parseInt(args[0]); + int n1 = Integer.parseInt(args[1]) + 1; // Add 1 to include the upper bound + // Add all integers in the range [n0, n1) + for (int n : IntStream.range(n0, n1).toArray()) { + values.add(n); + } + continue; + } catch (NumberFormatException e) { + // Handle invalid number format if necessary + throw new NumberFormatException("Invalid number format in range string: " + o); + } + } + } + } + + // Convert List<Integer> to a primitive int array and return it + return values.stream().mapToInt(Integer::intValue).toArray(); + } } diff --git a/src/main/resources/addon.yml b/src/main/resources/addon.yml index 69fdc96..576d843 100644 --- a/src/main/resources/addon.yml +++ b/src/main/resources/addon.yml @@ -19,7 +19,7 @@ authors: - BONNe # Soft dependencies of current addon. -softdepend: AcidIsland, BSkyBlock, CaveBlock, SkyGrid, AOneBlock +softdepend: AcidIsland, BSkyBlock, CaveBlock, SkyGrid, AOneBlock, ItemsAdder # List of addon permissions permissions: diff --git a/src/main/resources/controlPanelTemplate.yml b/src/main/resources/controlPanelTemplate.yml index b4aa32f..58347ae 100644 --- a/src/main/resources/controlPanelTemplate.yml +++ b/src/main/resources/controlPanelTemplate.yml @@ -5,7 +5,12 @@ # [server] - Command will be run by console, f.e. '[server] op [player]' will result in '/op BONNe1704' # [label] - [label] in command will be replaced with corresponding GameMode user command, # f.e. '[label] challenges' in BSkyblock will result in 'island challenges' -# material is used from Matherial.match +# Command types: +# command - Executed on left click (default click action) +# right_click_command - Executed on right click or shift+right click. Falls back to 'command' if not set. +# shift_click_command - Executed on shift+left click. Falls back to 'command' if not set. +# material is used from Material.match +# icon is used by parsing BentoBox ItemParser. Replacement for material. # permission is a suffix that will be added to the end of "[gamemode].controlpanel.panel.[suffix]". # Adding permission means that user will open control panel defined by permission. # If user will have multiple panel permissions, it will open first encountered with default flag. @@ -17,67 +22,77 @@ panel-list: default: defaultPanel: true - panelName: '&1Commands' + panelName: '&0&l Control Panel' permission: 'default' buttons: 0: - name: 'Island' - material: GRASS - description: - - '&1Go to &5 your island' + name: '&f&l Island' + material: GRASS_BLOCK + icon: minecraft:grass_block + description: |- + &7 Go to your island' command: '[label] go' 8: - name: 'Settings' - material: LAVA_BUCKET - description: - - 'Open your island settings' + name: '&f&l Settings' + material: ANVIL + icon: minecraft:anvil + description: |- + &7 Open your island settings command: '[label] settings' 3: - name: 'Team' - material: SKULL_ITEM - description: - - 'List team members' + name: '&f&l Team' + material: PLAYER_HEAD + icon: minecraft:player_head + description: |- + &7 List team members command: '[label] team' 1: - name: 'Set Home' + name: '&f&l Set Home' material: WHITE_BED - description: - - 'Set your home here' + icon: minecraft:white_bed + description: |- + &7 Set your home here command: '[label] sethome' 9: - name: 'Calculate Level' + name: '&f&l Calculate Level' material: ACACIA_STAIRS - description: - - 'Island level' - - 'Level: %Level_[gamemode]_island_level%' + icon: minecraft:acacia_stairs + description: |- + &7 Island level + &7 Level: &5 %Level_[gamemode]_island_level% command: '[label] level' 10: - name: 'Show top 10' + name: '&f&l Show top 10' material: BOOK - description: - - 'List the Top 10 islands' + icon: minecraft:book + description: |- + &7 Open top 10 islands command: '[label] top' 12: - name: 'Show warps' + name: '&f&l Show warps' material: OAK_SIGN - description: - - 'List warps available' + icon: minecraft:oak_sign + description: |- + &7 List warps available command: '[label] warps' 5: - name: 'Spawn' + name: '&f&l Spawn' material: BEDROCK - description: - - 'Go to world spawn' + icon: minecraft:bedrock + description: |- + &7 Go to world spawn command: '[label] spawn' 14: - name: 'Biomes' + name: '&f&l Biomes' material: OAK_SAPLING - description: - - 'Biomes' + icon: minecraft:oak_sapling + description: |- + &7 Change biome command: '[label] biomes' 15: - name: 'Challenges' + name: '&f&l Challenges' material: ENCHANTING_TABLE - description: - - 'Challenges' + icon: minecraft:enchanting_table + description: |- + &7 Open challenges command: '[label] challenges' \ No newline at end of file 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()! + * <p> + * 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<Bukkit> mockedBukkit; + protected MockedStatic<Util> 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<Island> 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<String>) invocation -> invocation.getArgument(1, String.class)); + when(plugin.getPlaceholdersManager()).thenReturn(phm); + when(phm.replacePlaceholders(any(), any())).thenAnswer((Answer<String>) 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<TextComponent> captor = ArgumentCaptor.forClass(TextComponent.class); + verify(spigot, atLeast(0)).sendMessage(captor.capture()); + + List<TextComponent> 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<Block> list) { + return new EntityExplodeEvent(entity, l, list, 0, null); + } + + public PlayerDeathEvent getPlayerDeathEvent(Player player, List<ItemStack> 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<DatabaseSetup> 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<Object> 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<Config> mockedConfig = Mockito.mockConstruction(Config.class, + (mock, context) -> when(mock.loadConfigObject()).thenReturn(settings))) { + addon.onLoad(); + assertNotNull(addon.getSettings()); + } + } + + @Test + void testOnLoadFailNullSettings() { + try (MockedConstruction<Config> 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<Config> 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<Config> 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<Config> 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<Config> 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<Flag, Integer> getDefaultIslandFlags() { + return Collections.emptyMap(); + } + + @Override + public Map<Flag, Integer> 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<String> 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<EntityType> getRemoveMobsWhitelist() { + return Collections.emptySet(); + } + + @Override + public int getSeaHeight() { + return 0; + } + + @Override + public List<String> getHiddenFlags() { + return Collections.emptyList(); + } + + @Override + public List<String> getVisitorBannedCommands() { + return Collections.emptyList(); + } + + @Override + public Map<String, Boolean> 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<String> 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<String> getOnLeaveCommands() { + return Collections.emptyList(); + } + + @Override + public boolean isUseOwnGenerator() { + return false; + } + + @Override + public boolean isWaterUnsafe() { + return false; + } + + @Override + public List<String> 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<ControlPanelGenerator> 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<String> 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..624f54c --- /dev/null +++ b/src/test/java/world/bentobox/controlpanel/database/objects/ControlPanelObjectTest.java @@ -0,0 +1,118 @@ +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<ControlPanelButton> 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.getRightClickCommand()); + button.setRightClickCommand("island settings"); + assertEquals("island settings", button.getRightClickCommand()); + + assertNull(button.getShiftClickCommand()); + button.setShiftClickCommand("island team"); + assertEquals("island team", button.getShiftClickCommand()); + + assertNull(button.getDescriptionLines()); + List<String> 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<Utils> mockedUtils; + private MockedStatic<DatabaseSetup> mockedDbSetup; + private AbstractDatabaseHandler<Object> 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<PanelBuilder> 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<ControlPanelButton> buttons = new ArrayList<>(); + buttons.add(button); + controlPanel.setPanelButtons(buttons); + + User user = User.getInstance(mockPlayer); + + try (MockedConstruction<PanelBuilder> 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<ControlPanelButton> buttons = new ArrayList<>(); + buttons.add(button); + controlPanel.setPanelButtons(buttons); + + User user = User.getInstance(mockPlayer); + + try (MockedConstruction<PanelBuilder> 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<ControlPanelButton> buttons = new ArrayList<>(); + buttons.add(button); + controlPanel.setPanelButtons(buttons); + + User user = User.getInstance(mockPlayer); + + try (MockedConstruction<PanelBuilder> 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<String> result = GuiUtils.stringSplit("Hello world", 999, false); + assertFalse(result.isEmpty()); + assertEquals("Hello world", result.get(0)); + } + + @Test + void testStringSplitWithPipe() { + List<String> 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<String> input = Arrays.asList("Hello", "World"); + List<String> result = GuiUtils.stringSplit(input, 999); + assertEquals(2, result.size()); + } + + @Test + void testStringSplitEmptyList() { + List<String> input = Collections.emptyList(); + List<String> 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); + } +}