From c3b531f50f06ffd4a389fc481f3f89688ff795e5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 16:22:38 +0000 Subject: [PATCH] Fix unsafe async API usage and race conditions in MedalShowCmd Refactored MedalShowCmd to schedule broadcastMessage and sendMessage calls on the main thread using Bukkit.getScheduler().runTask(). Removed shared state (sender, args) from MedalShowCmd to fix race conditions during concurrent command execution. Added JUnit 5 and Mockito dependencies and a unit test to verify the fix and prevent regressions. Modified SQLManager to allow dependency injection for testing. Co-authored-by: acsoto <59144459+acsoto@users.noreply.github.com> --- modules/MedalCabinet/pom.xml | 36 +++++- .../medalcabinet/command/MedalShowCmd.java | 24 ++-- .../mcatk/medalcabinet/sql/SQLManager.java | 7 +- .../command/MedalShowCmdTest.java | 105 ++++++++++++++++++ 4 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 modules/MedalCabinet/src/test/java/com/mcatk/medalcabinet/command/MedalShowCmdTest.java diff --git a/modules/MedalCabinet/pom.xml b/modules/MedalCabinet/pom.xml index 904e558..8909b44 100644 --- a/modules/MedalCabinet/pom.xml +++ b/modules/MedalCabinet/pom.xml @@ -66,6 +66,10 @@ placeholderapi https://repo.extendedclip.com/content/repositories/placeholderapi/ + + helpch + https://repo.helpch.at/releases/ + @@ -78,8 +82,38 @@ me.clip placeholderapi - 2.11.1 + 2.11.5 provided + + org.junit.jupiter + junit-jupiter + 5.9.2 + test + + + org.mockito + mockito-core + 5.2.0 + test + + + org.mockito + mockito-inline + 5.2.0 + test + + + net.bytebuddy + byte-buddy + 1.14.19 + test + + + net.bytebuddy + byte-buddy-agent + 1.14.19 + test + diff --git a/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/command/MedalShowCmd.java b/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/command/MedalShowCmd.java index da25743..1ad6e4e 100644 --- a/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/command/MedalShowCmd.java +++ b/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/command/MedalShowCmd.java @@ -10,12 +10,10 @@ import org.bukkit.entity.Player; public class MedalShowCmd implements CommandExecutor { - private CommandSender sender; - private String[] args; private final String prefix = "§7[§6勋章墙§7]§7 "; - private void printHelp() { + private void printHelp(CommandSender sender) { sender.sendMessage("§e------------帮助------------"); sender.sendMessage("§a/medalshow all §2展示你全部的勋章(全服可见)"); sender.sendMessage("§a/medalshow me §2展示你全部的勋章(仅自己可见)"); @@ -23,42 +21,44 @@ private void printHelp() { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - this.sender = sender; - this.args = args; if (!(sender instanceof Player)) { return false; } if (args.length == 0) { - printHelp(); + printHelp(sender); return true; } if ("all".equals(args[0])) { - showAll(); + showAll(sender); } else if ("me".equals(args[0])) { - showMe(); + showMe(sender); } return false; } - private void showAll() { + private void showAll(CommandSender sender) { Bukkit.getScheduler().runTaskAsynchronously(MedalCabinet.getPlugin(), () -> { StringBuilder stringBuilder = new StringBuilder(prefix).append("§e").append(sender.getName()) .append(" 展示了他的勋章:\n"); for (Medal medal : SQLManager.getInstance().getPlayerMedals(sender.getName())) { stringBuilder.append(medal).append(" "); } - MedalCabinet.getPlugin().getServer().broadcastMessage(stringBuilder.toString()); + Bukkit.getScheduler().runTask(MedalCabinet.getPlugin(), () -> { + MedalCabinet.getPlugin().getServer().broadcastMessage(stringBuilder.toString()); + }); }); } - private void showMe() { + private void showMe(CommandSender sender) { Bukkit.getScheduler().runTaskAsynchronously(MedalCabinet.getPlugin(), () -> { StringBuilder stringBuilder = new StringBuilder(prefix).append("§e").append(sender.getName()) .append(" 的勋章:\n"); for (Medal medal : SQLManager.getInstance().getPlayerMedals(sender.getName())) { stringBuilder.append(medal).append(" "); } - sender.sendMessage(stringBuilder.toString()); + Bukkit.getScheduler().runTask(MedalCabinet.getPlugin(), () -> { + sender.sendMessage(stringBuilder.toString()); + }); }); } diff --git a/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/sql/SQLManager.java b/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/sql/SQLManager.java index a719230..5cdcc20 100644 --- a/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/sql/SQLManager.java +++ b/modules/MedalCabinet/src/main/java/com/mcatk/medalcabinet/sql/SQLManager.java @@ -15,7 +15,12 @@ public static SQLManager getInstance() { return instance == null ? instance = new SQLManager() : instance; } - private SQLManager() { + // For testing + public static void setInstance(SQLManager instance) { + SQLManager.instance = instance; + } + + protected SQLManager() { connectMySQL(); } diff --git a/modules/MedalCabinet/src/test/java/com/mcatk/medalcabinet/command/MedalShowCmdTest.java b/modules/MedalCabinet/src/test/java/com/mcatk/medalcabinet/command/MedalShowCmdTest.java new file mode 100644 index 0000000..f2a15ec --- /dev/null +++ b/modules/MedalCabinet/src/test/java/com/mcatk/medalcabinet/command/MedalShowCmdTest.java @@ -0,0 +1,105 @@ +package com.mcatk.medalcabinet.command; + +import com.mcatk.medalcabinet.MedalCabinet; +import com.mcatk.medalcabinet.medal.Medal; +import com.mcatk.medalcabinet.sql.SQLManager; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.command.Command; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +public class MedalShowCmdTest { + + private MockedStatic mockedBukkit; + private MockedStatic mockedSQLManagerStatic; + private MockedStatic mockedMedalCabinetStatic; + private SQLManager mockedSQLManager; + private MedalCabinet mockedPlugin; + private Server mockedServer; + private BukkitScheduler mockedScheduler; + private Player mockedPlayer; + + @BeforeEach + public void setUp() { + mockedBukkit = mockStatic(Bukkit.class); + mockedSQLManagerStatic = mockStatic(SQLManager.class); + mockedMedalCabinetStatic = mockStatic(MedalCabinet.class); + + mockedPlugin = mock(MedalCabinet.class); + mockedServer = mock(Server.class); + mockedScheduler = mock(BukkitScheduler.class); + mockedPlayer = mock(Player.class); + mockedSQLManager = mock(SQLManager.class); + + // Setup Bukkit statics + mockedBukkit.when(Bukkit::getServer).thenReturn(mockedServer); + mockedBukkit.when(Bukkit::getScheduler).thenReturn(mockedScheduler); + + // Setup SQLManager singleton + mockedSQLManagerStatic.when(SQLManager::getInstance).thenReturn(mockedSQLManager); + + // Setup Plugin + mockedMedalCabinetStatic.when(MedalCabinet::getPlugin).thenReturn(mockedPlugin); + when(mockedPlugin.getServer()).thenReturn(mockedServer); + + // Setup Player + when(mockedPlayer.getName()).thenReturn("TestPlayer"); + } + + @AfterEach + public void tearDown() { + mockedBukkit.close(); + mockedSQLManagerStatic.close(); + mockedMedalCabinetStatic.close(); + } + + @Test + public void testShowAll() { + MedalShowCmd cmd = new MedalShowCmd(); + String[] args = {"all"}; + + // Mock SQL return + ArrayList medals = new ArrayList<>(); + medals.add(new Medal("1", "TestMedal", "STONE", "Desc")); + when(mockedSQLManager.getPlayerMedals("TestPlayer")).thenReturn(medals); + + // Capture the async task + ArgumentCaptor asyncTaskCaptor = ArgumentCaptor.forClass(Runnable.class); + when(mockedScheduler.runTaskAsynchronously(eq(mockedPlugin), asyncTaskCaptor.capture())) + .thenReturn(mock(BukkitTask.class)); + + // Execute command + cmd.onCommand(mockedPlayer, mock(Command.class), "medalshow", args); + + // Run the captured async task + Runnable asyncTask = asyncTaskCaptor.getValue(); + asyncTask.run(); + + // Verify that runTask (sync) was called + ArgumentCaptor syncTaskCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockedScheduler).runTask(eq(mockedPlugin), syncTaskCaptor.capture()); + + // Run the captured sync task + Runnable syncTask = syncTaskCaptor.getValue(); + syncTask.run(); + + // Verify broadcastMessage is called + verify(mockedServer).broadcastMessage(contains("TestPlayer 展示了他的勋章")); + } +}