diff --git a/build.gradle.kts b/build.gradle.kts index 96479d6..89519e8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -100,6 +100,7 @@ dependencies { testImplementation("org.mockito:mockito-core:5.19.0") testImplementation("org.mockito:mockito-junit-jupiter:5.19.0") + testImplementation("org.sayandev:sayanvanish-api:1.7.0-SNAPSHOT") testImplementation("net.luckperms:api:5.4") testImplementation("net.kyori:adventure-text-serializer-plain:$adventureVersion") testImplementation("io.github.miniplaceholders:miniplaceholders-api:3.0.1") diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/bungee/abstraction/BungeePlayer.java b/src/main/java/xyz/earthcow/networkjoinmessages/bungee/abstraction/BungeePlayer.java index 33fc178..ca87008 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/bungee/abstraction/BungeePlayer.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/bungee/abstraction/BungeePlayer.java @@ -1,6 +1,5 @@ package xyz.earthcow.networkjoinmessages.bungee.abstraction; -import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.Server; @@ -12,18 +11,15 @@ import java.util.UUID; -public class BungeePlayer implements CorePlayer { +public class BungeePlayer extends CorePlayer { private final ProxiedPlayer bungeePlayer; - private CoreBackendServer lastKnownConnectedServer; - private final Audience audience; - private String cachedLeaveMessage; - private boolean disconnecting = false; - private boolean premiumVanishHidden = false; public BungeePlayer(ProxiedPlayer bungeePlayer) { + super( + new BungeeServer(bungeePlayer.getServer().getInfo()), + BungeeMain.getInstance().getAudiences().player(bungeePlayer) + ); this.bungeePlayer = bungeePlayer; - this.lastKnownConnectedServer = new BungeeServer(bungeePlayer.getServer().getInfo()); - this.audience = BungeeMain.getInstance().getAudiences().player(bungeePlayer); } @Override @@ -33,7 +29,7 @@ public String getName() { @Override public void sendMessage(Component component) { - audience.sendMessage(component); + getAudience().sendMessage(component); } @Override @@ -56,56 +52,13 @@ public int getConnectionIdentity() { public @Nullable CoreBackendServer getCurrentServer() { Server server = bungeePlayer.getServer(); if (server == null) { - return lastKnownConnectedServer; + return getLastKnownConnectedServer(); } return new BungeeServer(server.getInfo()); } - @Override - public @Nullable CoreBackendServer getLastKnownConnectedServer() { - return lastKnownConnectedServer; - } - - @Override - public void setLastKnownConnectedServer(CoreBackendServer server) { - lastKnownConnectedServer = server; - } - - @Override - public @NotNull Audience getAudience() { - return audience; - } - @Override public boolean isInLimbo() { return false; } - - @Override - public String getCachedLeaveMessage() { - return cachedLeaveMessage; - } - @Override - public void setCachedLeaveMessage(String cachedLeaveMessage) { - this.cachedLeaveMessage = cachedLeaveMessage; - } - - @Override - public boolean isDisconnecting() { - return disconnecting; - } - @Override - public void setDisconnecting() { - this.disconnecting = true; - } - - @Override - public boolean getPremiumVanishHidden() { - return premiumVanishHidden; - } - - @Override - public void setPremiumVanishHidden(boolean premiumVanishHidden) { - this.premiumVanishHidden = premiumVanishHidden; - } } diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java index 860b302..0b9f8f4 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java @@ -81,7 +81,7 @@ public Core(CorePlugin plugin, PremiumVanish premiumVanish) { // Message building MessageFormatter messageFormatter = new MessageFormatter(plugin, config, sayanVanishHook); - ReceiverResolver receiverResolver = new ReceiverResolver(plugin, config); + ReceiverResolver receiverResolver = new ReceiverResolver(plugin, config, sayanVanishHook != null, premiumVanish != null); MessageHandler messageHandler = new MessageHandler(plugin, config, stateStore, placeholderResolver, receiverResolver); // Player event helpers diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/MessageHandler.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/MessageHandler.java index ac98440..d648b95 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/MessageHandler.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/MessageHandler.java @@ -9,7 +9,6 @@ import xyz.earthcow.networkjoinmessages.common.config.PluginConfig; import xyz.earthcow.networkjoinmessages.common.player.PlayerStateStore; import xyz.earthcow.networkjoinmessages.common.util.Formatter; -import xyz.earthcow.networkjoinmessages.common.MessageType; import xyz.earthcow.networkjoinmessages.common.util.PlaceholderResolver; import java.util.*; @@ -99,11 +98,11 @@ public void broadcastMessage(String text, MessageType type, String from, String public void broadcastMessage( String text, MessageType type, String from, String to, - @Nullable CorePlayer parseTarget, + @NotNull CorePlayer parseTarget, boolean silent ) { if (silent) { - broadcastSilentMessage(text, type, from, to, parseTarget); + broadcastSilentMessage(text, type, from, to, parseTarget, true); return; } @@ -127,19 +126,19 @@ public void broadcastMessage( } } - private void broadcastSilentMessage( - @NotNull String text, @NotNull MessageType type, - @NotNull String from, @NotNull String to, - @Nullable CorePlayer parseTarget + public void broadcastSilentMessage( + @NotNull String text, @NotNull MessageType type, + @NotNull String from, @NotNull String to, + @NotNull CorePlayer triggerPlayer, + boolean isParseTarget ) { - sendSilentConsoleMessage(type, from, to, parseTarget); + CorePlayer parseTarget = isParseTarget ? triggerPlayer : null; - if (!config.isNotifyAdminsOnSilentMove()) return; + sendSilentConsoleMessage(type, from, to, parseTarget); for (CorePlayer player : plugin.getAllPlayers()) { - if (player.hasPermission("networkjoinmessages.silent")) { - sendMessage(player, config.getSilentPrefix() + text, parseTarget); - } + if (!receiverResolver.isSilentReceiver(player, triggerPlayer)) continue; + sendMessage(player, config.getSilentPrefix() + text, parseTarget); } } diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java index eb91b16..59a0600 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java @@ -1,36 +1,34 @@ package xyz.earthcow.networkjoinmessages.common.abstraction; +import lombok.Getter; +import lombok.Setter; import net.kyori.adventure.audience.Audience; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.UUID; -public interface CorePlayer extends CoreCommandSender { +@Getter @Setter +public abstract class CorePlayer implements CoreCommandSender { + // Fields + private CoreBackendServer lastKnownConnectedServer; + private boolean disconnecting = false; + private String cachedLeaveMessage; + private Audience audience; + private boolean premiumVanishHidden = false; + private int premiumVanishUseLevel = 0; + private int premiumVanishSeeLevel = 0; + + public CorePlayer(CoreBackendServer lastKnownConnectedServer, Audience audience) { + this.lastKnownConnectedServer = lastKnownConnectedServer; + this.audience = audience; + } + + // Abstract @NotNull - UUID getUniqueId(); - - int getConnectionIdentity(); - - @Nullable - CoreBackendServer getCurrentServer(); - + public abstract UUID getUniqueId(); + public abstract int getConnectionIdentity(); @Nullable - CoreBackendServer getLastKnownConnectedServer(); - - void setLastKnownConnectedServer(CoreBackendServer server); - - @NotNull - Audience getAudience(); - - boolean isInLimbo(); - - String getCachedLeaveMessage(); - void setCachedLeaveMessage(String cachedLeaveMessage); - - boolean isDisconnecting(); - void setDisconnecting(); - - boolean getPremiumVanishHidden(); - void setPremiumVanishHidden(boolean premiumVanishHidden); + public abstract CoreBackendServer getCurrentServer(); + public abstract boolean isInLimbo(); } diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/broadcast/ReceiverResolver.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/broadcast/ReceiverResolver.java index 22ed049..112481a 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/broadcast/ReceiverResolver.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/broadcast/ReceiverResolver.java @@ -1,5 +1,6 @@ package xyz.earthcow.networkjoinmessages.common.broadcast; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import xyz.earthcow.networkjoinmessages.common.abstraction.CoreBackendServer; import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlayer; @@ -18,10 +19,15 @@ public final class ReceiverResolver { private final CorePlugin plugin; private final PluginConfig config; + private final boolean hasSayanVanish; + private final boolean hasPremiumVanish; - public ReceiverResolver(CorePlugin plugin, PluginConfig config) { + + public ReceiverResolver(CorePlugin plugin, PluginConfig config, boolean hasSayanVanish, boolean hasPremiumVanish) { this.plugin = plugin; this.config = config; + this.hasSayanVanish = hasSayanVanish; + this.hasPremiumVanish = hasPremiumVanish; } // --- Audience resolution --- @@ -77,6 +83,42 @@ private List resolve( return receivers; } + /** + * Determines if a player should receive a silent message.
+ * A player will receive a silent message if any one of the following is true: + *
    + *
  1. If {@code NotifyAdminsOnSilentMove} is enabled and the player holds the + * {@code networkjoinmessages.silent} permission
  2. + *
  3. If SayanVanish is present, {@code SVNotifyVanishEnabledPlayersOnSilentMove} is true, and the player holds + * the {@code sayanvanish.vanish.use} permission
  4. + *
  5. If PremiumVanish is present, {@code PVNotifyVanishEnabledPlayersOnSilentMove} is true, then if + * {@code PVNotifyRespectVanishLevels}: + *
      + *
    • Is true and the player's {@code pv.see} level is the same as or greater than the trigger player's + * {@code pv.use} level
    • + *
    • Is false and the player holds either {@code pv.use} or {@code pv.see}
    • + *
    + *
  6. + *
+ * @param player The player to determine whether they are a silent receiver or not + * @param triggerPlayer The player who triggered a message + * @return Whether the player is a silent receiver (true) or not (false) + */ + public boolean isSilentReceiver(@NotNull CorePlayer player, @NotNull CorePlayer triggerPlayer) { + if (config.isNotifyAdminsOnSilentMove() && player.hasPermission("networkjoinmessages.silent")) + return true; + if (hasSayanVanish && config.isSVNotifyVanishEnabledPlayersOnSilentMove() + && player.hasPermission("sayanvanish.vanish.use")) + return true; + if (hasPremiumVanish && config.isPVNotifyVanishEnabledPlayersOnSilentMove()) { + if (config.isPVNotifyRespectVanishLevels()) { + return player.getPremiumVanishSeeLevel() >= triggerPlayer.getPremiumVanishUseLevel(); + } + return (player.hasPermission("pv.use") || player.hasPermission("pv.see")); + } + return false; + } + // --- Blacklist / whitelist checks --- /** diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java index 5567271..cda800a 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/config/PluginConfig.java @@ -119,11 +119,14 @@ public final class PluginConfig { // Third-party plugin integration flags @Getter private boolean SVTreatVanishedPlayersAsSilent; @Getter private boolean SVRemoveVanishedPlayersFromPlayerCount; + @Getter private boolean SVNotifyVanishEnabledPlayersOnSilentMove; @Getter private boolean PVTreatVanishedPlayersAsSilent; @Getter private boolean PVRemoveVanishedPlayersFromPlayerCount; @Getter private boolean PVSpoofJoinMessageOnShow; @Getter private boolean PVSpoofLeaveMessageOnHide; @Getter private boolean PVTreatVanishedOnJoin; + @Getter private boolean PVNotifyVanishEnabledPlayersOnSilentMove; + @Getter private boolean PVNotifyRespectVanishLevels; @Getter private boolean shouldSuppressLimboSwap; @Getter private boolean shouldSuppressLimboJoin; @Getter private boolean shouldSuppressLimboLeave; @@ -219,17 +222,20 @@ public void reload() { serverJoinMessageDisabled = config.getStringList("Settings.IgnoreJoinMessagesList"); serverLeaveMessageDisabled = config.getStringList("Settings.IgnoreLeaveMessagesList"); - PPBRequestTimeout = config.getLong("OtherPlugins.PAPIProxyBridge.RequestTimeout"); - SVTreatVanishedPlayersAsSilent = config.getBoolean("OtherPlugins.SayanVanish.TreatVanishedPlayersAsSilent"); - SVRemoveVanishedPlayersFromPlayerCount = config.getBoolean("OtherPlugins.SayanVanish.RemoveVanishedPlayersFromPlayerCount"); - PVTreatVanishedPlayersAsSilent = config.getBoolean("OtherPlugins.PremiumVanish.TreatVanishedPlayersAsSilent"); - PVRemoveVanishedPlayersFromPlayerCount = config.getBoolean("OtherPlugins.PremiumVanish.RemoveVanishedPlayersFromPlayerCount"); - PVSpoofJoinMessageOnShow = config.getBoolean("OtherPlugins.PremiumVanish.SpoofJoinMessageOnShow"); - PVSpoofLeaveMessageOnHide = config.getBoolean("OtherPlugins.PremiumVanish.SpoofLeaveMessageOnHide"); - PVTreatVanishedOnJoin = config.getBoolean("OtherPlugins.PremiumVanish.TreatVanishedOnJoin"); - shouldSuppressLimboSwap = config.getBoolean("OtherPlugins.LimboAPI.SuppressSwapMessages"); - shouldSuppressLimboJoin = config.getBoolean("OtherPlugins.LimboAPI.SuppressJoinMessages"); - shouldSuppressLimboLeave = config.getBoolean("OtherPlugins.LimboAPI.SuppressLeaveMessages"); + PPBRequestTimeout = config.getLong("OtherPlugins.PAPIProxyBridge.RequestTimeout"); + SVTreatVanishedPlayersAsSilent = config.getBoolean("OtherPlugins.SayanVanish.TreatVanishedPlayersAsSilent"); + SVRemoveVanishedPlayersFromPlayerCount = config.getBoolean("OtherPlugins.SayanVanish.RemoveVanishedPlayersFromPlayerCount"); + SVNotifyVanishEnabledPlayersOnSilentMove = config.getBoolean("OtherPlugins.SayanVanish.NotifyVanishEnabledPlayersOnSilentMove"); + PVTreatVanishedPlayersAsSilent = config.getBoolean("OtherPlugins.PremiumVanish.TreatVanishedPlayersAsSilent"); + PVRemoveVanishedPlayersFromPlayerCount = config.getBoolean("OtherPlugins.PremiumVanish.RemoveVanishedPlayersFromPlayerCount"); + PVSpoofJoinMessageOnShow = config.getBoolean("OtherPlugins.PremiumVanish.SpoofJoinMessageOnShow"); + PVSpoofLeaveMessageOnHide = config.getBoolean("OtherPlugins.PremiumVanish.SpoofLeaveMessageOnHide"); + PVTreatVanishedOnJoin = config.getBoolean("OtherPlugins.PremiumVanish.TreatVanishedOnJoin"); + PVNotifyVanishEnabledPlayersOnSilentMove = config.getBoolean("OtherPlugins.PremiumVanish.NotifyVanishEnabledPlayersOnSilentMove"); + PVNotifyRespectVanishLevels = config.getBoolean("OtherPlugins.PremiumVanish.NotifyRespectVanishLevels"); + shouldSuppressLimboSwap = config.getBoolean("OtherPlugins.LimboAPI.SuppressSwapMessages"); + shouldSuppressLimboJoin = config.getBoolean("OtherPlugins.LimboAPI.SuppressJoinMessages"); + shouldSuppressLimboLeave = config.getBoolean("OtherPlugins.LimboAPI.SuppressLeaveMessages"); plugin.getCoreLogger().setDebug(config.getBoolean("debug")); diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java index 5a7115d..ca80b15 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java @@ -16,6 +16,7 @@ import xyz.earthcow.networkjoinmessages.common.storage.PlayerJoinTracker; import xyz.earthcow.networkjoinmessages.common.MessageType; import xyz.earthcow.networkjoinmessages.common.util.PlaceholderResolver; +import xyz.earthcow.networkjoinmessages.common.util.PremiumVanishLevelUtil; /** * Routes platform-level player events (join, swap, disconnect) to the appropriate handlers. @@ -100,7 +101,7 @@ public void onDisconnect(@NotNull CorePlayer player) { plugin.getCoreLogger().debug("Duplicate disconnect ignored for " + player.getName()); return; } - player.setDisconnecting(); + player.setDisconnecting(true); if (shouldSkipLeave(player)) { cleanup(player); @@ -119,8 +120,13 @@ private void handleJoin(@NotNull CorePlayer player, @NotNull CoreBackendServer s player.setLastKnownConnectedServer(server); PremiumVanish pv = plugin.getVanishAPI(); - if (pv != null && pv.isVanished(player.getUniqueId())) { - player.setPremiumVanishHidden(true); + if (pv != null) { + if (config.isPVNotifyVanishEnabledPlayersOnSilentMove() && config.isPVNotifyRespectVanishLevels()) { + PremiumVanishLevelUtil.updateVanishLevels(player); + } + if (pv.isVanished(player.getUniqueId())) { + player.setPremiumVanishHidden(true); + } } leaveMessageCache.refresh(player); @@ -166,8 +172,8 @@ private void broadcastLeave(@NotNull CorePlayer player) { boolean silent = silenceChecker.isSilent(player); String serverName = player.getCurrentServer().getName(); - // Pass null as parseTarget — player is gone, placeholders already resolved in cache - messageHandler.broadcastMessage(message, MessageType.LEAVE, serverName, "", null, silent); + // Pass player as triggerPlayer but false for isParseTarget as the placeholders are already resolved in cache + messageHandler.broadcastSilentMessage(message, MessageType.LEAVE, serverName, "", player, false); fireLeaveEvent(player, serverName, message, silent); } diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePremiumVanishListener.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePremiumVanishListener.java index 71d241b..c7185ce 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePremiumVanishListener.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePremiumVanishListener.java @@ -22,7 +22,7 @@ public CorePremiumVanishListener(@NotNull CoreLogger logger, @NotNull PluginConf } public void handlePremiumVanishShow(@NotNull CorePlayer player) { - if (!player.getPremiumVanishHidden()) return; + if (!player.isPremiumVanishHidden()) return; logger.debug("Setting PremiumVanishHidden to FALSE for " + player.getName()); player.setPremiumVanishHidden(false); if (config.isPVSpoofJoinMessageOnShow() && !stateStore.getSilentState(player)) { @@ -31,7 +31,7 @@ public void handlePremiumVanishShow(@NotNull CorePlayer player) { } public void handlePremiumVanishHide(@NotNull CorePlayer player) { - if (player.getPremiumVanishHidden()) return; + if (player.isPremiumVanishHidden()) return; logger.debug("Setting PremiumVanishHidden to TRUE for " + player.getName()); player.setPremiumVanishHidden(true); if (config.isPVSpoofLeaveMessageOnHide() && !stateStore.getSilentState(player)) { diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/player/SilenceChecker.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/player/SilenceChecker.java index 84d5c6e..59f96ac 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/player/SilenceChecker.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/player/SilenceChecker.java @@ -74,7 +74,7 @@ private boolean isSayanVanishSilent(CorePlayer player) { private boolean isPremiumVanishSilent(CorePlayer player) { return premiumVanish != null && config.isPVTreatVanishedPlayersAsSilent() - && (premiumVanish.isVanished(player.getUniqueId()) || player.getPremiumVanishHidden()); + && (premiumVanish.isVanished(player.getUniqueId()) || player.isPremiumVanishHidden()); } private void logDebugState(CorePlayer player) { @@ -88,7 +88,7 @@ private void logDebugState(CorePlayer player) { premiumVanish != null, config.isPVTreatVanishedPlayersAsSilent(), premiumVanish != null ? premiumVanish.isVanished(player.getUniqueId()) : "N/A", - player.getPremiumVanishHidden(), + player.isPremiumVanishHidden(), config.isPVTreatVanishedOnJoin(), player.hasPermission(PV_JOIN_VANISHED_PERM) )); diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/util/PremiumVanishLevelUtil.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/util/PremiumVanishLevelUtil.java new file mode 100644 index 0000000..06b3c5b --- /dev/null +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/util/PremiumVanishLevelUtil.java @@ -0,0 +1,40 @@ +package xyz.earthcow.networkjoinmessages.common.util; + +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlayer; + +public final class PremiumVanishLevelUtil { + + private static final int MAX_LEVEL = 100; + + /** + * Determines a player's vanish level by scanning down from {@link PremiumVanishLevelUtil#MAX_LEVEL} to obtain + * their highest level + * @param player The player to analyze + * @param forUse If {@code true}, we use {@code pv.use} otherwise we use {@code pv.see} + * @return The determined highest vanish level for the specified player + */ + private static int determineVanishLevel(CorePlayer player, boolean forUse) { + String base = forUse ? "pv.use" : "pv.see"; + String prefix = base + ".level"; + for (int i = MAX_LEVEL; i >= 1; i--) { + if (player.hasPermission(prefix + i)) { + return i; + } + } + // The base permission (pv.use or pv.see) is a level of 1 + return player.hasPermission(base) ? 1 : 0; + } + + /** + * Updates a player's PremiumVanish use and see vanish levels. Uses + * {@link PremiumVanishLevelUtil#determineVanishLevel(CorePlayer, boolean)} to do so. The logic is unpreventably + * costly as it iterates down from {@link PremiumVanishLevelUtil#MAX_LEVEL} checking player permission each time. + * Should be called asynchronously to the main thread. + * @param player The player in which vanish levels will be updated for + */ + public static void updateVanishLevels(CorePlayer player) { + player.setPremiumVanishUseLevel(determineVanishLevel(player, true)); + player.setPremiumVanishSeeLevel(determineVanishLevel(player, false)); + } + +} diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/velocity/abstraction/VelocityPlayer.java b/src/main/java/xyz/earthcow/networkjoinmessages/velocity/abstraction/VelocityPlayer.java index 8a2ee92..84e9005 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/velocity/abstraction/VelocityPlayer.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/velocity/abstraction/VelocityPlayer.java @@ -13,20 +13,17 @@ import java.util.UUID; -public class VelocityPlayer implements CorePlayer { +public class VelocityPlayer extends CorePlayer { private final Player velocityPlayer; - private CoreBackendServer lastKnownConnectedServer; - private final Audience audience; - private String cachedLeaveMessage; - private boolean disconnecting = false; - private boolean premiumVanishHidden = false; public VelocityPlayer(Player velocityPlayer) { + super( + velocityPlayer.getCurrentServer().isPresent() ? + new VelocityServer(velocityPlayer.getCurrentServer().get().getServer()) + : null + , Audience.audience(velocityPlayer) + ); this.velocityPlayer = velocityPlayer; - if (velocityPlayer.getCurrentServer().isPresent()) { - this.lastKnownConnectedServer = new VelocityServer(velocityPlayer.getCurrentServer().get().getServer()); - } - this.audience = Audience.audience(velocityPlayer); } @Override @@ -59,26 +56,11 @@ public int getConnectionIdentity() { public @Nullable CoreBackendServer getCurrentServer() { ServerConnection serverConnection = velocityPlayer.getCurrentServer().orElse(null); if (serverConnection == null) { - return lastKnownConnectedServer; + return getLastKnownConnectedServer(); } return new VelocityServer(serverConnection.getServer()); } - @Override - public @Nullable CoreBackendServer getLastKnownConnectedServer() { - return lastKnownConnectedServer; - } - - @Override - public void setLastKnownConnectedServer(CoreBackendServer server) { - lastKnownConnectedServer = server; - } - - @Override - public @NotNull Audience getAudience() { - return audience; - } - @Override public boolean isInLimbo() { if (!VelocityMain.getInstance().getIsLimboAPIAvailable()) { @@ -87,33 +69,4 @@ public boolean isInLimbo() { //noinspection ConstantValue return ((ConnectedPlayer) velocityPlayer).getConnection().getState().name() == null; } - - @Override - public String getCachedLeaveMessage() { - return cachedLeaveMessage; - } - @Override - public void setCachedLeaveMessage(String cachedLeaveMessage) { - this.cachedLeaveMessage = cachedLeaveMessage; - } - - @Override - public boolean isDisconnecting() { - return disconnecting; - } - - @Override - public void setDisconnecting() { - this.disconnecting = true; - } - - @Override - public boolean getPremiumVanishHidden() { - return premiumVanishHidden; - } - - @Override - public void setPremiumVanishHidden(boolean premiumVanishHidden) { - this.premiumVanishHidden = premiumVanishHidden; - } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 25f3bb8..31a8d31 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -244,6 +244,8 @@ OtherPlugins: TreatVanishedPlayersAsSilent: true # Vanished players will not be counted in the player count placeholders RemoveVanishedPlayersFromPlayerCount: true + # Send silent messages to players with the permission sayanvanish.vanish.use (permission must be set on the proxy) + NotifyVanishEnabledPlayersOnSilentMove: false PremiumVanish: # Vanished players will not trigger messages aside from # silent messages sent to players with networkjoinmessages.silent permission @@ -257,6 +259,13 @@ OtherPlugins: # Treats a player with the permission pv.joinvanished as vanished on join (permission must be set on the proxy) # This is to be used in conjunction with the PremiumVanish 'AutoVanishOnJoin' option TreatVanishedOnJoin: false + # Send silent messages to players with the permission pv.use or pv.see (permission must be set on the proxy) + NotifyVanishEnabledPlayersOnSilentMove: false + # Instead of sending silent messages to all players with pv.use or pv.see, only send silent messages to players with + # a greater than or equal to see level than the player who triggered the message's use level. + # Simply put, this setting respects the layered use/see permissions set with PremiumVanish. + # Requires players to rejoin upon changes to their level or setting this setting to true + NotifyRespectVanishLevels: false LimboAPI: # No swap messages for players swapping to or from limbo servers SuppressSwapMessages: true @@ -267,4 +276,4 @@ OtherPlugins: debug: false # Do not touch this -config-version: 13 \ No newline at end of file +config-version: 14 \ No newline at end of file diff --git a/src/test/java/xyz/earthcow/networkjoinmessages/common/broadcast/MessageFormatterTest.java b/src/test/java/xyz/earthcow/networkjoinmessages/common/broadcast/MessageFormatterTest.java new file mode 100644 index 0000000..1ec019c --- /dev/null +++ b/src/test/java/xyz/earthcow/networkjoinmessages/common/broadcast/MessageFormatterTest.java @@ -0,0 +1,314 @@ +package xyz.earthcow.networkjoinmessages.common.broadcast; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import xyz.earthcow.networkjoinmessages.common.abstraction.CoreBackendServer; +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlayer; +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlugin; +import xyz.earthcow.networkjoinmessages.common.abstraction.PremiumVanish; +import xyz.earthcow.networkjoinmessages.common.config.PluginConfig; +import xyz.earthcow.networkjoinmessages.common.modules.SayanVanishHook; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class MessageFormatterTest { + + @Mock private CorePlugin plugin; + @Mock private PluginConfig config; + @Mock private CorePlayer player; + @Mock private CoreBackendServer server; + @Mock private SayanVanishHook sayanVanish; + @Mock private PremiumVanish premiumVanish; + + private final UUID playerUuid = UUID.randomUUID(); + + // A second online player used for counting tests + @Mock private CorePlayer otherPlayer; + private final UUID otherUuid = UUID.randomUUID(); + + @BeforeEach + void setup() { + when(player.getUniqueId()).thenReturn(playerUuid); + when(player.getCurrentServer()).thenReturn(server); + when(server.getName()).thenReturn("lobby"); + when(server.getPlayersConnected()).thenReturn(List.of(player)); + + when(otherPlayer.getUniqueId()).thenReturn(otherUuid); + + when(plugin.getServer("lobby")).thenReturn(server); + when(plugin.getAllPlayers()).thenReturn(List.of(player)); + + // Disable vanish integrations by default + when(config.isPVRemoveVanishedPlayersFromPlayerCount()).thenReturn(false); + when(config.isSVRemoveVanishedPlayersFromPlayerCount()).thenReturn(false); + when(plugin.getVanishAPI()).thenReturn(null); + } + + // ----------------------------------------------------------------------- + // formatJoinMessage + // ----------------------------------------------------------------------- + + @Test + void formatJoinMessage_noPlaceholders_returnsRaw() { + when(config.getJoinNetworkMessage()).thenReturn("Welcome!"); + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + assertEquals("Welcome!", fmt.formatJoinMessage(player)); + } + + @Test + void formatJoinMessage_playerCountServer_playerAlreadyPresent_joining() { + // Player is in the server list. Joining means count is used as-is (player IS already counted). + when(config.getJoinNetworkMessage()).thenReturn("%playercount_server% players online"); + when(server.getPlayersConnected()).thenReturn(List.of(player)); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + // 1 player on server, not leaving => count stays 1 + assertEquals("1 players online", fmt.formatJoinMessage(player)); + } + + @Test + void formatJoinMessage_playerCountNetwork() { + when(config.getJoinNetworkMessage()).thenReturn("%playercount_network% online"); + when(plugin.getAllPlayers()).thenReturn(List.of(player)); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + assertEquals("1 online", fmt.formatJoinMessage(player)); + } + + @Test + void formatJoinMessage_playerNotYetInList_countIsIncrementedByOne() { + // Simulates the state just before the player is added to the server list + when(config.getJoinNetworkMessage()).thenReturn("%playercount_server% players"); + when(server.getPlayersConnected()).thenReturn(Collections.emptyList()); // player not yet counted + when(plugin.getServer("lobby")).thenReturn(server); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + // Player absent + !leaving => count should be 0+1 = 1 + assertEquals("1 players", fmt.formatJoinMessage(player)); + } + + // ----------------------------------------------------------------------- + // formatLeaveMessage + // ----------------------------------------------------------------------- + + @Test + void formatLeaveMessage_playerCountServer_leavingReducesCountByOne() { + when(config.getLeaveNetworkMessage()).thenReturn("%playercount_server% remain"); + when(server.getPlayersConnected()).thenReturn(List.of(player)); // still in list at leave time + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + // Player present + leaving => 1-1 = 0 + assertEquals("0 remain", fmt.formatLeaveMessage(player)); + } + + @Test + void formatLeaveMessage_noPlaceholders_returnsRaw() { + when(config.getLeaveNetworkMessage()).thenReturn("Goodbye!"); + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + assertEquals("Goodbye!", fmt.formatLeaveMessage(player)); + } + + // ----------------------------------------------------------------------- + // formatFirstJoinMessage + // ----------------------------------------------------------------------- + + @Test + void formatFirstJoinMessage_playerCountServer_delegatesToJoinLogic() { + when(config.getFirstJoinNetworkMessage()).thenReturn("%playercount_server% players"); + when(server.getPlayersConnected()).thenReturn(Collections.emptyList()); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + // Player absent, joining => 0+1 = 1 + assertEquals("1 players", fmt.formatFirstJoinMessage(player)); + } + + // ----------------------------------------------------------------------- + // formatSwapMessage + // ----------------------------------------------------------------------- + + @Test + void formatSwapMessage_serverNamePlaceholders() { + when(config.getSwapServerMessage()).thenReturn("%from% -> %to%"); + when(config.getServerDisplayName("lobby")).thenReturn("Lobby"); + when(config.getServerDisplayName("survival")).thenReturn("Survival"); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + assertEquals("Lobby -> Survival", fmt.formatSwapMessage(player, "lobby", "survival")); + } + + @Test + void formatSwapMessage_cleanServerNamePlaceholders() { + when(config.getSwapServerMessage()).thenReturn("%from_clean% to %to_clean%"); + when(config.getServerDisplayName(anyString())).thenAnswer(inv -> inv.getArgument(0)); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + assertEquals("lobby to survival", fmt.formatSwapMessage(player, "lobby", "survival")); + } + + @Test + void formatSwapMessage_playerCountFromPlaceholder() { + when(config.getSwapServerMessage()).thenReturn("%playercount_from% leaving"); + when(config.getServerDisplayName(anyString())).thenAnswer(inv -> inv.getArgument(0)); + + CoreBackendServer fromServer = mock(CoreBackendServer.class); + when(fromServer.getPlayersConnected()).thenReturn(List.of(player)); + when(plugin.getServer("lobby")).thenReturn(fromServer); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + // player IS in fromServer list, leaving => 1-1 = 0 + assertEquals("0 leaving", fmt.formatSwapMessage(player, "lobby", "survival")); + } + + @Test + void formatSwapMessage_playerCountToPlaceholder() { + when(config.getSwapServerMessage()).thenReturn("%playercount_to% in dest"); + when(config.getServerDisplayName(anyString())).thenAnswer(inv -> inv.getArgument(0)); + + CoreBackendServer toServer = mock(CoreBackendServer.class); + when(toServer.getPlayersConnected()).thenReturn(Collections.emptyList()); + when(plugin.getServer("survival")).thenReturn(toServer); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + // player NOT in toServer, not leaving => 0+1 = 1 + assertEquals("1 in dest", fmt.formatSwapMessage(player, "lobby", "survival")); + } + + @Test + void formatSwapMessage_networkCountPlaceholder() { + when(config.getSwapServerMessage()).thenReturn("%playercount_network% total"); + when(config.getServerDisplayName(anyString())).thenAnswer(inv -> inv.getArgument(0)); + when(plugin.getAllPlayers()).thenReturn(List.of(player, otherPlayer)); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + // 2 players, player is present and NOT leaving => count = 2 + assertEquals("2 total", fmt.formatSwapMessage(player, "lobby", "survival")); + } + + // ----------------------------------------------------------------------- + // computePlayerCount -- vanish integration + // ----------------------------------------------------------------------- + + @Test + void playerCount_vanishedPlayerIsExcludedWhenPVEnabled() { + when(config.getJoinNetworkMessage()).thenReturn("%playercount_network% online"); + when(config.isPVRemoveVanishedPlayersFromPlayerCount()).thenReturn(true); + when(plugin.getVanishAPI()).thenReturn(premiumVanish); + // Both players online, but otherPlayer is vanished + when(plugin.getAllPlayers()).thenReturn(List.of(player, otherPlayer)); + when(premiumVanish.getInvisiblePlayers()).thenReturn(List.of(otherUuid)); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + // Only player is visible; player not present in server list, joining => 0+1 = 1 + assertEquals("1 online", fmt.formatJoinMessage(player)); + } + + @Test + void playerCount_vanishedPlayerIsExcludedWhenSVEnabled() { + when(config.getJoinNetworkMessage()).thenReturn("%playercount_network% online"); + when(config.isSVRemoveVanishedPlayersFromPlayerCount()).thenReturn(true); + when(plugin.getAllPlayers()).thenReturn(List.of(player, otherPlayer)); + when(sayanVanish.getVanishedPlayers()).thenReturn(List.of(otherUuid)); + + MessageFormatter fmt = new MessageFormatter(plugin, config, sayanVanish); + // player joining (absent from list), other is vanished + when(plugin.getAllPlayers()).thenReturn(List.of(otherPlayer)); + // only otherPlayer in list, otherPlayer is vanished => visible = 0; subject absent, joining => 1 + assertEquals("1 online", fmt.formatJoinMessage(player)); + } + + @Test + void playerCount_vanishedSubjectIsNotCountedForOthers() { + when(config.getJoinNetworkMessage()).thenReturn("%playercount_network% online"); + when(config.isPVRemoveVanishedPlayersFromPlayerCount()).thenReturn(true); + when(plugin.getVanishAPI()).thenReturn(premiumVanish); + when(plugin.getAllPlayers()).thenReturn(List.of(player)); + // Subject (player) is vanished + when(premiumVanish.getInvisiblePlayers()).thenReturn(List.of(playerUuid)); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + // player is vanished => visible list is empty, and subject is vanished so no +1 => 0 + assertEquals("0 online", fmt.formatJoinMessage(player)); + } + + // ----------------------------------------------------------------------- + // getServerPlayerCount -- null server + // ----------------------------------------------------------------------- + + @Test + void getServerPlayerCount_nullServer_leavingReturnsZero() { + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + String result = fmt.getServerPlayerCount( + (CoreBackendServer) null, true, player); + assertEquals("0", result); + } + + @Test + void getServerPlayerCount_nullServer_joiningReturnsOne() { + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + String result = fmt.getServerPlayerCount( + (CoreBackendServer) null, false, player); + assertEquals("1", result); + } + + // ----------------------------------------------------------------------- + // prepareDiscordJoinLeaveTemplate + // ----------------------------------------------------------------------- + + @Test + void prepareDiscordJoinLeaveTemplate_replacesAvatarUrl() { + when(config.isPVRemoveVanishedPlayersFromPlayerCount()).thenReturn(false); + when(config.isSVRemoveVanishedPlayersFromPlayerCount()).thenReturn(false); + when(plugin.getVanishAPI()).thenReturn(null); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + String result = fmt.prepareDiscordJoinLeaveTemplate( + "Avatar: %embedavatarurl%", player, false, "https://example.com/avatar.png"); + assertEquals("Avatar: https://example.com/avatar.png", result); + } + + @Test + void prepareDiscordJoinLeaveTemplate_replacesPlayerCountServer() { + when(config.isPVRemoveVanishedPlayersFromPlayerCount()).thenReturn(false); + when(config.isSVRemoveVanishedPlayersFromPlayerCount()).thenReturn(false); + when(plugin.getVanishAPI()).thenReturn(null); + when(server.getPlayersConnected()).thenReturn(List.of(player)); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + String result = fmt.prepareDiscordJoinLeaveTemplate( + "%playercount_server% online", player, false, ""); + // player present, joining (not leaving): 1 + assertEquals("1 online", result); + } + + // ----------------------------------------------------------------------- + // prepareDiscordSwapTemplate + // ----------------------------------------------------------------------- + + @Test + void prepareDiscordSwapTemplate_replacesAllServerPlaceholders() { + when(config.getServerDisplayName("lobby")).thenReturn("Lobby"); + when(config.getServerDisplayName("survival")).thenReturn("Survival"); + when(config.isPVRemoveVanishedPlayersFromPlayerCount()).thenReturn(false); + when(config.isSVRemoveVanishedPlayersFromPlayerCount()).thenReturn(false); + when(plugin.getVanishAPI()).thenReturn(null); + + MessageFormatter fmt = new MessageFormatter(plugin, config, null); + String result = fmt.prepareDiscordSwapTemplate( + "%from% -> %to% (%from_clean% to %to_clean%) %embedavatarurl%", + player, "lobby", "survival", "http://img"); + + assertEquals("Lobby -> Survival (lobby to survival) http://img", result); + } +} diff --git a/src/test/java/xyz/earthcow/networkjoinmessages/common/broadcast/ReceiverResolverTest.java b/src/test/java/xyz/earthcow/networkjoinmessages/common/broadcast/ReceiverResolverTest.java new file mode 100644 index 0000000..8ed98ac --- /dev/null +++ b/src/test/java/xyz/earthcow/networkjoinmessages/common/broadcast/ReceiverResolverTest.java @@ -0,0 +1,456 @@ +package xyz.earthcow.networkjoinmessages.common.broadcast; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import xyz.earthcow.networkjoinmessages.common.MessageType; +import xyz.earthcow.networkjoinmessages.common.abstraction.CoreBackendServer; +import xyz.earthcow.networkjoinmessages.common.abstraction.CoreLogger; +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlayer; +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlugin; +import xyz.earthcow.networkjoinmessages.common.config.PluginConfig; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class ReceiverResolverTest { + + @Mock private CorePlugin plugin; + @Mock private PluginConfig config; + @Mock private CoreLogger logger; + @Mock private CorePlayer lobbyPlayer; + @Mock private CorePlayer survivalPlayer; + @Mock private CorePlayer hubPlayer; + @Mock private CoreBackendServer lobbyServer; + @Mock private CoreBackendServer survivalServer; + @Mock private CoreBackendServer hubServer; + + private final UUID lobbyUuid = UUID.randomUUID(); + private final UUID survivalUuid = UUID.randomUUID(); + private final UUID hubUuid = UUID.randomUUID(); + + @BeforeEach + void setup() { + when(plugin.getCoreLogger()).thenReturn(logger); + + when(lobbyPlayer.getUniqueId()).thenReturn(lobbyUuid); + when(survivalPlayer.getUniqueId()).thenReturn(survivalUuid); + when(hubPlayer.getUniqueId()).thenReturn(hubUuid); + + when(lobbyPlayer.getCurrentServer()).thenReturn(lobbyServer); + when(survivalPlayer.getCurrentServer()).thenReturn(survivalServer); + when(hubPlayer.getCurrentServer()).thenReturn(hubServer); + + when(lobbyServer.getName()).thenReturn("lobby"); + when(survivalServer.getName()).thenReturn("survival"); + when(hubServer.getName()).thenReturn("hub"); + + when(lobbyServer.getPlayersConnected()).thenReturn(List.of(lobbyPlayer)); + when(survivalServer.getPlayersConnected()).thenReturn(List.of(survivalPlayer)); + when(hubServer.getPlayersConnected()).thenReturn(List.of(hubPlayer)); + + when(plugin.getServer("lobby")).thenReturn(lobbyServer); + when(plugin.getServer("survival")).thenReturn(survivalServer); + when(plugin.getServer("hub")).thenReturn(hubServer); + when(plugin.getAllPlayers()).thenReturn(List.of(lobbyPlayer, survivalPlayer, hubPlayer)); + + // Default: no blacklisted servers, blacklist mode + when(config.getBlacklistedServers()).thenReturn(Collections.emptyList()); + when(config.isUseBlacklistAsWhitelist()).thenReturn(false); + when(config.getSwapServerMessageRequires()).thenReturn("ANY"); + + // Default suppression lists empty + when(config.getServerFirstJoinMessageDisabled()).thenReturn(Collections.emptyList()); + when(config.getServerJoinMessageDisabled()).thenReturn(Collections.emptyList()); + when(config.getServerLeaveMessageDisabled()).thenReturn(Collections.emptyList()); + } + + // ----------------------------------------------------------------------- + // getJoinReceivers -- viewableByJoined / viewableByOther combinations + // ----------------------------------------------------------------------- + + @Test + void getJoinReceivers_allFlagsTrue_returnsAllPlayers() { + when(config.isJoinViewableByJoined()).thenReturn(true); + when(config.isJoinViewableByOther()).thenReturn(true); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + List receivers = resolver.getJoinReceivers("lobby"); + + assertEquals(3, receivers.size()); + } + + @Test + void getJoinReceivers_onlyJoinedCanSee_returnsLobbyPlayersOnly() { + when(config.isJoinViewableByJoined()).thenReturn(true); + when(config.isJoinViewableByOther()).thenReturn(false); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + List receivers = resolver.getJoinReceivers("lobby"); + + assertEquals(1, receivers.size()); + assertTrue(receivers.contains(lobbyPlayer)); + } + + @Test + void getJoinReceivers_onlyOthersCanSee_excludesLobbyPlayers() { + when(config.isJoinViewableByJoined()).thenReturn(false); + when(config.isJoinViewableByOther()).thenReturn(true); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + List receivers = resolver.getJoinReceivers("lobby"); + + assertFalse(receivers.contains(lobbyPlayer)); + assertTrue(receivers.contains(survivalPlayer)); + assertTrue(receivers.contains(hubPlayer)); + } + + @Test + void getJoinReceivers_allFlagsFalse_returnsEmpty() { + when(config.isJoinViewableByJoined()).thenReturn(false); + when(config.isJoinViewableByOther()).thenReturn(false); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + List receivers = resolver.getJoinReceivers("lobby"); + + assertTrue(receivers.isEmpty()); + } + + // ----------------------------------------------------------------------- + // getLeaveReceivers + // ----------------------------------------------------------------------- + + @Test + void getLeaveReceivers_viewableByLeftOnly_returnsOriginServer() { + when(config.isLeaveViewableByLeft()).thenReturn(true); + when(config.isLeaveViewableByOther()).thenReturn(false); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + List receivers = resolver.getLeaveReceivers("lobby"); + + assertEquals(1, receivers.size()); + assertTrue(receivers.contains(lobbyPlayer)); + } + + @Test + void getLeaveReceivers_viewableByOtherOnly_excludesOriginServer() { + when(config.isLeaveViewableByLeft()).thenReturn(false); + when(config.isLeaveViewableByOther()).thenReturn(true); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + List receivers = resolver.getLeaveReceivers("lobby"); + + assertFalse(receivers.contains(lobbyPlayer)); + assertTrue(receivers.contains(survivalPlayer)); + } + + // ----------------------------------------------------------------------- + // getSwapReceivers + // ----------------------------------------------------------------------- + + @Test + void getSwapReceivers_allFlagsTrue_returnsAllPlayers() { + when(config.isSwapViewableByJoined()).thenReturn(true); + when(config.isSwapViewableByLeft()).thenReturn(true); + when(config.isSwapViewableByOther()).thenReturn(true); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + List receivers = resolver.getSwapReceivers("survival", "lobby"); + + assertEquals(3, receivers.size()); + } + + @Test + void getSwapReceivers_joinedAndLeftOnly_returnsOriginAndDestination() { + when(config.isSwapViewableByJoined()).thenReturn(true); + when(config.isSwapViewableByLeft()).thenReturn(true); + when(config.isSwapViewableByOther()).thenReturn(false); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + List receivers = resolver.getSwapReceivers("survival", "lobby"); + + assertTrue(receivers.contains(survivalPlayer)); + assertTrue(receivers.contains(lobbyPlayer)); + assertFalse(receivers.contains(hubPlayer)); + } + + @Test + void getSwapReceivers_othersOnly_excludesBothEndpoints() { + when(config.isSwapViewableByJoined()).thenReturn(false); + when(config.isSwapViewableByLeft()).thenReturn(false); + when(config.isSwapViewableByOther()).thenReturn(true); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + List receivers = resolver.getSwapReceivers("survival", "lobby"); + + assertFalse(receivers.contains(survivalPlayer)); + assertFalse(receivers.contains(lobbyPlayer)); + assertTrue(receivers.contains(hubPlayer)); + } + + // ----------------------------------------------------------------------- + // getFirstJoinReceivers + // ----------------------------------------------------------------------- + + @Test + void getFirstJoinReceivers_joinedAndOther_returnsAll() { + when(config.isFirstJoinViewableByJoined()).thenReturn(true); + when(config.isFirstJoinViewableByOther()).thenReturn(true); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + List receivers = resolver.getFirstJoinReceivers("lobby"); + + assertEquals(3, receivers.size()); + } + + // ----------------------------------------------------------------------- + // isBlacklisted(CorePlayer) -- blacklist mode + // ----------------------------------------------------------------------- + + @Test + void isBlacklisted_blacklistMode_serverIsListed_returnsTrue() { + when(config.getBlacklistedServers()).thenReturn(List.of("lobby")); + when(config.isUseBlacklistAsWhitelist()).thenReturn(false); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + assertTrue(resolver.isBlacklisted(lobbyPlayer)); + } + + @Test + void isBlacklisted_blacklistMode_serverNotListed_returnsFalse() { + when(config.getBlacklistedServers()).thenReturn(List.of("lobby")); + when(config.isUseBlacklistAsWhitelist()).thenReturn(false); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + assertFalse(resolver.isBlacklisted(survivalPlayer)); + } + + // ----------------------------------------------------------------------- + // isBlacklisted(CorePlayer) -- whitelist mode + // ----------------------------------------------------------------------- + + @Test + void isBlacklisted_whitelistMode_serverIsListed_returnsFalse() { + when(config.getBlacklistedServers()).thenReturn(List.of("lobby")); + when(config.isUseBlacklistAsWhitelist()).thenReturn(true); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + // In whitelist mode: listed = allowed = NOT blacklisted + assertFalse(resolver.isBlacklisted(lobbyPlayer)); + } + + @Test + void isBlacklisted_whitelistMode_serverNotListed_returnsTrue() { + when(config.getBlacklistedServers()).thenReturn(List.of("lobby")); + when(config.isUseBlacklistAsWhitelist()).thenReturn(true); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + // survival is NOT listed => not in whitelist => blacklisted + assertTrue(resolver.isBlacklisted(survivalPlayer)); + } + + // ----------------------------------------------------------------------- + // isBlacklisted(from, to) -- swap requires modes + // ----------------------------------------------------------------------- + + @ParameterizedTest(name = "requires={0}, fromListed={1}, toListed={2}, whitelistMode={3}, expected={4}") + @CsvSource({ + // BLACKLIST mode + "ANY, true, false, false, true", // from listed => blocked + "ANY, false, true, false, true", // to listed => blocked + "ANY, false, false, false, false", // neither listed => not blocked + "BOTH, true, false, false, false", // only from listed, need both => not blocked + "BOTH, true, true, false, true", // both listed => blocked + "JOINED, false, true, false, true", // to (joined) listed => blocked + "JOINED, true, false, false, false",// only from listed => not blocked for JOINED + "LEFT, true, false, false, true", // from (left) listed => blocked + "LEFT, false, true, false, false", // only to listed => not blocked for LEFT + // WHITELIST mode + "ANY, false, false, true, true", // neither in whitelist => blocked + "ANY, true, true, true, false", // both in whitelist => not blocked (ANY: from||to listed = true; whitelist XOR => false) + }) + void isBlacklisted_swap_requiresModes( + String requires, boolean fromListed, boolean toListed, + boolean whitelistMode, boolean expected) { + + String from = fromListed ? "lobby" : "hub"; + String to = toListed ? "survival" : "other"; + + // Configure listed servers: always include lobby and survival in the list + when(config.getBlacklistedServers()).thenReturn(List.of("lobby", "survival")); + when(config.isUseBlacklistAsWhitelist()).thenReturn(whitelistMode); + when(config.getSwapServerMessageRequires()).thenReturn(requires); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + assertEquals(expected, resolver.isBlacklisted(from, to)); + } + + @Test + void isBlacklisted_swap_unknownRequiresValue_returnsFalseInBlacklistMode() { + when(config.getBlacklistedServers()).thenReturn(List.of("lobby")); + when(config.isUseBlacklistAsWhitelist()).thenReturn(false); + when(config.getSwapServerMessageRequires()).thenReturn("GARBAGE"); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + // result = XOR(false, false) = false... but in whitelist mode it would be true + // In blacklist mode: isUseBlacklistAsWhitelist=false XOR result=false => false + assertFalse(resolver.isBlacklisted("lobby", "survival")); + } + + @Test + void isBlacklisted_swap_nullServers_neverListed() { + when(config.getBlacklistedServers()).thenReturn(List.of("lobby")); + when(config.isUseBlacklistAsWhitelist()).thenReturn(false); + when(config.getSwapServerMessageRequires()).thenReturn("ANY"); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + assertFalse(resolver.isBlacklisted(null, null)); + } + + // ----------------------------------------------------------------------- + // getServerSuppressedPlayers + // ----------------------------------------------------------------------- + + @Test + void getServerSuppressedPlayers_joinType_returnsPlayersOnDisabledServers() { + when(config.getServerJoinMessageDisabled()).thenReturn(List.of("lobby")); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + Set suppressed = resolver.getServerSuppressedPlayers(MessageType.JOIN); + + assertTrue(suppressed.contains(lobbyUuid)); + assertFalse(suppressed.contains(survivalUuid)); + } + + @Test + void getServerSuppressedPlayers_leaveType_returnsPlayersOnDisabledServers() { + when(config.getServerLeaveMessageDisabled()).thenReturn(List.of("survival")); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + Set suppressed = resolver.getServerSuppressedPlayers(MessageType.LEAVE); + + assertTrue(suppressed.contains(survivalUuid)); + assertFalse(suppressed.contains(lobbyUuid)); + } + + @Test + void getServerSuppressedPlayers_firstJoinType_usesFirstJoinList() { + when(config.getServerFirstJoinMessageDisabled()).thenReturn(List.of("hub")); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + Set suppressed = resolver.getServerSuppressedPlayers(MessageType.FIRST_JOIN); + + assertTrue(suppressed.contains(hubUuid)); + assertFalse(suppressed.contains(lobbyUuid)); + } + + @Test + void getServerSuppressedPlayers_swapType_alwaysEmpty() { + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + Set suppressed = resolver.getServerSuppressedPlayers(MessageType.SWAP); + assertTrue(suppressed.isEmpty()); + } + + @Test + void getServerSuppressedPlayers_unknownServer_isSkipped() { + when(config.getServerJoinMessageDisabled()).thenReturn(List.of("nonexistent")); + when(plugin.getServer("nonexistent")).thenReturn(null); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + Set suppressed = resolver.getServerSuppressedPlayers(MessageType.JOIN); + assertTrue(suppressed.isEmpty()); + } + + // ----------------------------------------------------------------------- + // isSilentReceiver + // ----------------------------------------------------------------------- + + @Test + void isSilentReceiver_adminPermission_returnsTrue() { + when(config.isNotifyAdminsOnSilentMove()).thenReturn(true); + when(lobbyPlayer.hasPermission("networkjoinmessages.silent")).thenReturn(true); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + assertTrue(resolver.isSilentReceiver(lobbyPlayer, survivalPlayer)); + } + + @Test + void isSilentReceiver_adminPermDisabled_returnsFalse() { + when(config.isNotifyAdminsOnSilentMove()).thenReturn(false); + when(lobbyPlayer.hasPermission("networkjoinmessages.silent")).thenReturn(true); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + assertFalse(resolver.isSilentReceiver(lobbyPlayer, survivalPlayer)); + } + + @Test + void isSilentReceiver_sayanVanishEnabled_vanishPermission_returnsTrue() { + when(config.isNotifyAdminsOnSilentMove()).thenReturn(false); + when(config.isSVNotifyVanishEnabledPlayersOnSilentMove()).thenReturn(true); + when(lobbyPlayer.hasPermission("sayanvanish.vanish.use")).thenReturn(true); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, true, false); + assertTrue(resolver.isSilentReceiver(lobbyPlayer, survivalPlayer)); + } + + @Test + void isSilentReceiver_pvEnabled_noRespectLevels_pvUsePerm_returnsTrue() { + when(config.isNotifyAdminsOnSilentMove()).thenReturn(false); + when(config.isSVNotifyVanishEnabledPlayersOnSilentMove()).thenReturn(false); + when(config.isPVNotifyVanishEnabledPlayersOnSilentMove()).thenReturn(true); + when(config.isPVNotifyRespectVanishLevels()).thenReturn(false); + when(lobbyPlayer.hasPermission("pv.use")).thenReturn(true); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, true); + assertTrue(resolver.isSilentReceiver(lobbyPlayer, survivalPlayer)); + } + + @Test + void isSilentReceiver_pvEnabled_respectLevels_sufficientSeeLevel_returnsTrue() { + when(config.isNotifyAdminsOnSilentMove()).thenReturn(false); + when(config.isPVNotifyVanishEnabledPlayersOnSilentMove()).thenReturn(true); + when(config.isPVNotifyRespectVanishLevels()).thenReturn(true); + // observer see level 5, trigger player use level 5 => observer qualifies + when(lobbyPlayer.getPremiumVanishSeeLevel()).thenReturn(5); + when(survivalPlayer.getPremiumVanishUseLevel()).thenReturn(5); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, true); + assertTrue(resolver.isSilentReceiver(lobbyPlayer, survivalPlayer)); + } + + @Test + void isSilentReceiver_pvEnabled_respectLevels_insufficientSeeLevel_returnsFalse() { + when(config.isNotifyAdminsOnSilentMove()).thenReturn(false); + when(config.isPVNotifyVanishEnabledPlayersOnSilentMove()).thenReturn(true); + when(config.isPVNotifyRespectVanishLevels()).thenReturn(true); + // observer see level 3 < trigger use level 5 => does not qualify + when(lobbyPlayer.getPremiumVanishSeeLevel()).thenReturn(3); + when(survivalPlayer.getPremiumVanishUseLevel()).thenReturn(5); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, true); + assertFalse(resolver.isSilentReceiver(lobbyPlayer, survivalPlayer)); + } + + @Test + void isSilentReceiver_noConditionsMet_returnsFalse() { + when(config.isNotifyAdminsOnSilentMove()).thenReturn(false); + when(config.isSVNotifyVanishEnabledPlayersOnSilentMove()).thenReturn(false); + when(config.isPVNotifyVanishEnabledPlayersOnSilentMove()).thenReturn(false); + + ReceiverResolver resolver = new ReceiverResolver(plugin, config, false, false); + assertFalse(resolver.isSilentReceiver(lobbyPlayer, survivalPlayer)); + } +} diff --git a/src/test/java/xyz/earthcow/networkjoinmessages/common/player/LeaveJoinBufferManagerTest.java b/src/test/java/xyz/earthcow/networkjoinmessages/common/player/LeaveJoinBufferManagerTest.java new file mode 100644 index 0000000..be805b8 --- /dev/null +++ b/src/test/java/xyz/earthcow/networkjoinmessages/common/player/LeaveJoinBufferManagerTest.java @@ -0,0 +1,230 @@ +package xyz.earthcow.networkjoinmessages.common.player; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import xyz.earthcow.networkjoinmessages.common.abstraction.CoreLogger; +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlayer; +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlugin; +import xyz.earthcow.networkjoinmessages.common.config.PluginConfig; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class LeaveJoinBufferManagerTest { + + @Mock private CorePlugin plugin; + @Mock private CoreLogger logger; + @Mock private PluginConfig config; + @Mock private CorePlayer player; + + private final UUID playerUuid = UUID.randomUUID(); + + @BeforeEach + void setup() { + when(plugin.getCoreLogger()).thenReturn(logger); + when(player.getUniqueId()).thenReturn(playerUuid); + when(player.getName()).thenReturn("TestPlayer"); + } + + // ----------------------------------------------------------------------- + // isDisabled + // ----------------------------------------------------------------------- + + @Test + void isDisabled_zeroDuration_returnsTrue() { + when(config.getLeaveJoinBufferDuration()).thenReturn(0); + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + assertTrue(mgr.isDisabled()); + } + + @Test + void isDisabled_positiveDuration_returnsFalse() { + when(config.getLeaveJoinBufferDuration()).thenReturn(5000); + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + assertFalse(mgr.isDisabled()); + } + + @Test + void isDisabled_negativeDuration_returnsTrue() { + when(config.getLeaveJoinBufferDuration()).thenReturn(-1); + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + assertTrue(mgr.isDisabled()); + } + + // ----------------------------------------------------------------------- + // isPending -- before any schedule + // ----------------------------------------------------------------------- + + @Test + void isPending_noTaskScheduled_returnsFalse() { + when(config.getLeaveJoinBufferDuration()).thenReturn(5000); + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + assertFalse(mgr.isPending(player)); + } + + // ----------------------------------------------------------------------- + // scheduleLeave + // ----------------------------------------------------------------------- + + @Test + void scheduleLeave_schedulesAsyncTask() { + when(config.getLeaveJoinBufferDuration()).thenReturn(5000); + when(plugin.runTaskAsyncLater(any(), eq(5000))).thenReturn(42); + + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + mgr.scheduleLeave(player, () -> {}); + + verify(plugin).runTaskAsyncLater(any(), eq(5000)); + } + + @Test + void scheduleLeave_afterSchedule_isPendingReturnsTrue() { + when(config.getLeaveJoinBufferDuration()).thenReturn(5000); + when(plugin.runTaskAsyncLater(any(), anyInt())).thenReturn(42); + + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + mgr.scheduleLeave(player, () -> {}); + + assertTrue(mgr.isPending(player)); + } + + @Test + void scheduleLeave_whenTaskFires_callbackIsInvoked() { + when(config.getLeaveJoinBufferDuration()).thenReturn(5000); + + // Capture the Runnable so we can fire it manually + ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); + when(plugin.runTaskAsyncLater(runnableCaptor.capture(), anyInt())).thenReturn(42); + + AtomicBoolean callbackFired = new AtomicBoolean(false); + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + mgr.scheduleLeave(player, () -> callbackFired.set(true)); + + // Simulate the scheduler firing + runnableCaptor.getValue().run(); + + assertTrue(callbackFired.get(), "Leave callback should have been invoked"); + } + + @Test + void scheduleLeave_whenTaskFires_pendingEntryIsRemoved() { + when(config.getLeaveJoinBufferDuration()).thenReturn(5000); + + ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); + when(plugin.runTaskAsyncLater(runnableCaptor.capture(), anyInt())).thenReturn(42); + + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + mgr.scheduleLeave(player, () -> {}); + + runnableCaptor.getValue().run(); + + assertFalse(mgr.isPending(player), + "Pending entry should be removed after the task fires"); + } + + // ----------------------------------------------------------------------- + // cancelIfPending + // ----------------------------------------------------------------------- + + @Test + void cancelIfPending_noPendingTask_returnsFalse() { + when(config.getLeaveJoinBufferDuration()).thenReturn(5000); + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + assertFalse(mgr.cancelIfPending(player)); + } + + @Test + void cancelIfPending_hasPendingTask_returnsTrueAndCancels() { + when(config.getLeaveJoinBufferDuration()).thenReturn(5000); + when(plugin.runTaskAsyncLater(any(), anyInt())).thenReturn(99); + + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + mgr.scheduleLeave(player, () -> {}); + + boolean result = mgr.cancelIfPending(player); + + assertTrue(result); + verify(plugin).cancelTask(99); + } + + @Test + void cancelIfPending_hasPendingTask_removesPendingEntry() { + when(config.getLeaveJoinBufferDuration()).thenReturn(5000); + when(plugin.runTaskAsyncLater(any(), anyInt())).thenReturn(99); + + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + mgr.scheduleLeave(player, () -> {}); + mgr.cancelIfPending(player); + + assertFalse(mgr.isPending(player)); + } + + @Test + void cancelIfPending_calledTwice_secondCallReturnsFalse() { + when(config.getLeaveJoinBufferDuration()).thenReturn(5000); + when(plugin.runTaskAsyncLater(any(), anyInt())).thenReturn(99); + + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + mgr.scheduleLeave(player, () -> {}); + + assertTrue(mgr.cancelIfPending(player)); + assertFalse(mgr.cancelIfPending(player), "Second cancel on same player should return false"); + } + + // ----------------------------------------------------------------------- + // Multiple players -- independence + // ----------------------------------------------------------------------- + + @Test + void scheduleLeave_multiplePlayers_areTrackedIndependently() { + CorePlayer player2 = mock(CorePlayer.class); + UUID uuid2 = UUID.randomUUID(); + when(player2.getUniqueId()).thenReturn(uuid2); + when(player2.getName()).thenReturn("OtherPlayer"); + + when(config.getLeaveJoinBufferDuration()).thenReturn(5000); + when(plugin.runTaskAsyncLater(any(), anyInt())).thenReturn(1, 2); + + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + mgr.scheduleLeave(player, () -> {}); + mgr.scheduleLeave(player2, () -> {}); + + assertTrue(mgr.isPending(player)); + assertTrue(mgr.isPending(player2)); + + mgr.cancelIfPending(player); + + assertFalse(mgr.isPending(player)); + assertTrue(mgr.isPending(player2), "Cancelling player1 must not affect player2"); + } + + // ----------------------------------------------------------------------- + // scheduleLeave -- second call for same player overwrites first + // ----------------------------------------------------------------------- + + @Test + void scheduleLeave_calledTwiceForSamePlayer_overwritesTaskId() { + when(config.getLeaveJoinBufferDuration()).thenReturn(5000); + when(plugin.runTaskAsyncLater(any(), anyInt())).thenReturn(10, 20); + + LeaveJoinBufferManager mgr = new LeaveJoinBufferManager(plugin, config); + mgr.scheduleLeave(player, () -> {}); + mgr.scheduleLeave(player, () -> {}); // second schedule + + // cancelIfPending should cancel the LAST registered task (20) + mgr.cancelIfPending(player); + verify(plugin).cancelTask(20); + } +} diff --git a/src/test/java/xyz/earthcow/networkjoinmessages/common/player/PlayerStateStoreTest.java b/src/test/java/xyz/earthcow/networkjoinmessages/common/player/PlayerStateStoreTest.java new file mode 100644 index 0000000..545f7ef --- /dev/null +++ b/src/test/java/xyz/earthcow/networkjoinmessages/common/player/PlayerStateStoreTest.java @@ -0,0 +1,361 @@ +package xyz.earthcow.networkjoinmessages.common.player; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import xyz.earthcow.networkjoinmessages.common.MessageType; +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlayer; +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlugin; +import xyz.earthcow.networkjoinmessages.common.config.PluginConfig; +import xyz.earthcow.networkjoinmessages.common.storage.PlayerDataStore; +import xyz.earthcow.networkjoinmessages.common.util.PlayerDataSnapshot; + +import java.util.Set; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class PlayerStateStoreTest { + + @Mock private CorePlugin plugin; + @Mock private PluginConfig config; + @Mock private PlayerDataStore store; + @Mock private CorePlayer player; + + private final UUID playerUuid = UUID.randomUUID(); + + @BeforeEach + void setup() { + when(player.getUniqueId()).thenReturn(playerUuid); + when(player.getName()).thenReturn("TestPlayer"); + + // Default: all "ignore by default" flags OFF, silent default OFF + when(config.isIgnoreJoinByDefault()).thenReturn(false); + when(config.isIgnoreSwapByDefault()).thenReturn(false); + when(config.isIgnoreLeaveByDefault()).thenReturn(false); + when(config.isSilentJoinDefaultState()).thenReturn(false); + + // Async tasks: execute synchronously for testing + doAnswer(inv -> { ((Runnable) inv.getArgument(0)).run(); return null; }) + .when(plugin).runTaskAsync(any()); + } + + // ----------------------------------------------------------------------- + // loadData -- new player (null snapshot from store) + // ----------------------------------------------------------------------- + + @Test + void loadData_newPlayer_storeReturnsNull_nothingAddedToSuppression() { + when(store.getData(playerUuid)).thenReturn(null); + when(config.isIgnoreJoinByDefault()).thenReturn(false); + when(config.isIgnoreSwapByDefault()).thenReturn(false); + when(config.isIgnoreLeaveByDefault()).thenReturn(false); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.loadData(playerUuid, "TestPlayer"); + + // Messages should NOT be suppressed + assertFalse(stateStore.getSuppressedPlayers(MessageType.JOIN).contains(playerUuid)); + assertFalse(stateStore.getSuppressedPlayers(MessageType.SWAP).contains(playerUuid)); + assertFalse(stateStore.getSuppressedPlayers(MessageType.LEAVE).contains(playerUuid)); + } + + @Test + void loadData_newPlayer_ignoreJoinByDefault_suppressesJoin() { + when(store.getData(playerUuid)).thenReturn(null); + when(config.isIgnoreJoinByDefault()).thenReturn(true); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.loadData(playerUuid, "TestPlayer"); + + assertTrue(stateStore.getSuppressedPlayers(MessageType.JOIN).contains(playerUuid)); + } + + @Test + void loadData_newPlayer_ignoreSwapByDefault_suppressesSwap() { + when(store.getData(playerUuid)).thenReturn(null); + when(config.isIgnoreSwapByDefault()).thenReturn(true); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.loadData(playerUuid, "TestPlayer"); + + assertTrue(stateStore.getSuppressedPlayers(MessageType.SWAP).contains(playerUuid)); + } + + @Test + void loadData_newPlayer_ignoreLeaveByDefault_suppressesLeave() { + when(store.getData(playerUuid)).thenReturn(null); + when(config.isIgnoreLeaveByDefault()).thenReturn(true); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.loadData(playerUuid, "TestPlayer"); + + assertTrue(stateStore.getSuppressedPlayers(MessageType.LEAVE).contains(playerUuid)); + } + + @Test + void loadData_newPlayer_savesInitialRecord() { + when(store.getData(playerUuid)).thenReturn(null); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.loadData(playerUuid, "TestPlayer"); + + verify(store).saveData(eq(playerUuid), any(PlayerDataSnapshot.class)); + } + + // ----------------------------------------------------------------------- + // loadData -- returning player with stored snapshot + // ----------------------------------------------------------------------- + + @Test + void loadData_returningPlayer_silentStateRestored() { + PlayerDataSnapshot snapshot = new PlayerDataSnapshot("TestPlayer", true, null, null, null); + when(store.getData(playerUuid)).thenReturn(snapshot); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.loadData(playerUuid, "TestPlayer"); + + // Grant the permission so getSilentState checks the map + when(player.hasPermission("networkjoinmessages.silent")).thenReturn(true); + assertTrue(stateStore.getSilentState(player)); + } + + @Test + void loadData_returningPlayer_nullSilentState_usesDefault() { + PlayerDataSnapshot snapshot = new PlayerDataSnapshot("TestPlayer", null, null, null, null); + when(store.getData(playerUuid)).thenReturn(snapshot); + when(config.isSilentJoinDefaultState()).thenReturn(false); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.loadData(playerUuid, "TestPlayer"); + + when(player.hasPermission("networkjoinmessages.silent")).thenReturn(true); + assertFalse(stateStore.getSilentState(player)); + } + + @Test + void loadData_returningPlayer_explicitIgnoreJoinTrue_suppressesJoin() { + PlayerDataSnapshot snapshot = new PlayerDataSnapshot("TestPlayer", null, true, null, null); + when(store.getData(playerUuid)).thenReturn(snapshot); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.loadData(playerUuid, "TestPlayer"); + + assertTrue(stateStore.getSuppressedPlayers(MessageType.JOIN).contains(playerUuid)); + } + + @Test + void loadData_returningPlayer_explicitIgnoreJoinFalse_doesNotSuppressEvenWithDefault() { + when(config.isIgnoreJoinByDefault()).thenReturn(true); // default says suppress + PlayerDataSnapshot snapshot = new PlayerDataSnapshot("TestPlayer", null, false, null, null); + when(store.getData(playerUuid)).thenReturn(snapshot); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.loadData(playerUuid, "TestPlayer"); + + // Explicit false overrides the default + assertFalse(stateStore.getSuppressedPlayers(MessageType.JOIN).contains(playerUuid)); + } + + // ----------------------------------------------------------------------- + // getSilentState -- permission guard + // ----------------------------------------------------------------------- + + @Test + void getSilentState_playerLacksPermission_returnsFalseRegardlessOfStoredState() { + when(player.hasPermission("networkjoinmessages.silent")).thenReturn(false); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + // Even if the map had true stored for this UUID, no permission => false + assertFalse(stateStore.getSilentState(player)); + } + + @Test + void getSilentState_playerHasPermission_firstAccessUsesDefault() { + when(player.hasPermission("networkjoinmessages.silent")).thenReturn(true); + when(config.isSilentJoinDefaultState()).thenReturn(true); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + assertTrue(stateStore.getSilentState(player)); + } + + // ----------------------------------------------------------------------- + // setSilentState + // ----------------------------------------------------------------------- + + @Test + void setSilentState_updatesStateAndPersists() { + when(player.hasPermission("networkjoinmessages.silent")).thenReturn(true); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.setSilentState(player, true); + + assertTrue(stateStore.getSilentState(player)); + verify(store).saveData(eq(playerUuid), any(PlayerDataSnapshot.class)); + } + + @Test + void setSilentState_falseUpdatesAndPersists() { + when(player.hasPermission("networkjoinmessages.silent")).thenReturn(true); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.setSilentState(player, true); + stateStore.setSilentState(player, false); + + assertFalse(stateStore.getSilentState(player)); + } + + // ----------------------------------------------------------------------- + // setSendMessageState -- by string type + // ----------------------------------------------------------------------- + + @Test + void setSendMessageState_allFalse_suppressesAllTypes() { + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.setSendMessageState("all", player, false); + + assertTrue(stateStore.getSuppressedPlayers(MessageType.JOIN).contains(playerUuid)); + assertTrue(stateStore.getSuppressedPlayers(MessageType.SWAP).contains(playerUuid)); + assertTrue(stateStore.getSuppressedPlayers(MessageType.LEAVE).contains(playerUuid)); + } + + @Test + void setSendMessageState_allTrue_removesAllSuppression() { + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.setSendMessageState("all", player, false); // first suppress + stateStore.setSendMessageState("all", player, true); // then re-enable + + assertFalse(stateStore.getSuppressedPlayers(MessageType.JOIN).contains(playerUuid)); + assertFalse(stateStore.getSuppressedPlayers(MessageType.SWAP).contains(playerUuid)); + assertFalse(stateStore.getSuppressedPlayers(MessageType.LEAVE).contains(playerUuid)); + } + + @Test + void setSendMessageState_joinOnly_onlyJoinSuppressed() { + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.setSendMessageState("join", player, false); + + assertTrue(stateStore.getSuppressedPlayers(MessageType.JOIN).contains(playerUuid)); + assertFalse(stateStore.getSuppressedPlayers(MessageType.SWAP).contains(playerUuid)); + assertFalse(stateStore.getSuppressedPlayers(MessageType.LEAVE).contains(playerUuid)); + } + + @Test + void setSendMessageState_leaveOnly_onlyLeaveSuppressed() { + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.setSendMessageState("leave", player, false); + + assertFalse(stateStore.getSuppressedPlayers(MessageType.JOIN).contains(playerUuid)); + assertFalse(stateStore.getSuppressedPlayers(MessageType.SWAP).contains(playerUuid)); + assertTrue(stateStore.getSuppressedPlayers(MessageType.LEAVE).contains(playerUuid)); + } + + @Test + void setSendMessageState_swapOnly_onlySwapSuppressed() { + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.setSendMessageState("swap", player, false); + + assertFalse(stateStore.getSuppressedPlayers(MessageType.JOIN).contains(playerUuid)); + assertTrue(stateStore.getSuppressedPlayers(MessageType.SWAP).contains(playerUuid)); + assertFalse(stateStore.getSuppressedPlayers(MessageType.LEAVE).contains(playerUuid)); + } + + @Test + void setSendMessageState_persistsToStore() { + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.setSendMessageState("join", player, false); + + verify(store).saveData(eq(playerUuid), any(PlayerDataSnapshot.class)); + } + + // ----------------------------------------------------------------------- + // getSuppressedPlayers -- FIRST_JOIN maps to JOIN set + // ----------------------------------------------------------------------- + + @Test + void getSuppressedPlayers_firstJoinMapsToJoinSet() { + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.setSendMessageState("join", player, false); + + Set joinSuppressed = stateStore.getSuppressedPlayers(MessageType.JOIN); + Set firstJoinSuppressed = stateStore.getSuppressedPlayers(MessageType.FIRST_JOIN); + assertSame(joinSuppressed, firstJoinSuppressed, + "FIRST_JOIN should return the same set as JOIN"); + } + + // ----------------------------------------------------------------------- + // isConnected / setConnected + // ----------------------------------------------------------------------- + + @Test + void setConnected_true_marksPlayerOnline() { + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.setConnected(player, true); + assertTrue(stateStore.isConnected(player)); + } + + @Test + void setConnected_false_marksPlayerOffline() { + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.setConnected(player, true); + stateStore.setConnected(player, false); + assertFalse(stateStore.isConnected(player)); + } + + @Test + void isConnected_unknownPlayer_returnsFalse() { + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + assertFalse(stateStore.isConnected(player)); + } + + // ----------------------------------------------------------------------- + // getFrom / setFrom + // ----------------------------------------------------------------------- + + @Test + void setFrom_storesServerName() { + // This is only added so that player.getCurrentServer().getName() doesn't throw a NPE + // The only time player#getCurrentServer can possibly be null is the brief time between when they initially + // connect to when the server connected event is fired and CorePlayerListener#handleJoin is run + var mockServer = mock(xyz.earthcow.networkjoinmessages.common.abstraction.CoreBackendServer.class); + when(player.getCurrentServer()).thenReturn(mockServer); + when(mockServer.getName()).thenReturn(""); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + stateStore.setFrom(player, "lobby"); + assertEquals("lobby", stateStore.getFrom(player)); + } + + @Test + void getFrom_noEntryStored_fallsBackToCurrentServer() { + var mockServer = mock(xyz.earthcow.networkjoinmessages.common.abstraction.CoreBackendServer.class); + when(player.getCurrentServer()).thenReturn(mockServer); + when(mockServer.getName()).thenReturn("survival"); + + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); + assertEquals("survival", stateStore.getFrom(player)); + } + + // ----------------------------------------------------------------------- + // null store -- no NPE + // ----------------------------------------------------------------------- + + @Test + void loadData_nullStore_doesNotThrow() { + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, null); + assertDoesNotThrow(() -> stateStore.loadData(playerUuid, "TestPlayer")); + } + + @Test + void setSendMessageState_nullStore_doesNotThrow() { + PlayerStateStore stateStore = new PlayerStateStore(plugin, config, null); + assertDoesNotThrow(() -> stateStore.setSendMessageState("join", player, false)); + } +} diff --git a/src/test/java/xyz/earthcow/networkjoinmessages/common/player/SilenceCheckerTest.java b/src/test/java/xyz/earthcow/networkjoinmessages/common/player/SilenceCheckerTest.java new file mode 100644 index 0000000..5e70a61 --- /dev/null +++ b/src/test/java/xyz/earthcow/networkjoinmessages/common/player/SilenceCheckerTest.java @@ -0,0 +1,229 @@ +package xyz.earthcow.networkjoinmessages.common.player; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import xyz.earthcow.networkjoinmessages.common.abstraction.CoreLogger; +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlayer; +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlugin; +import xyz.earthcow.networkjoinmessages.common.abstraction.PremiumVanish; +import xyz.earthcow.networkjoinmessages.common.config.PluginConfig; +import xyz.earthcow.networkjoinmessages.common.modules.SayanVanishHook; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class SilenceCheckerTest { + + @Mock private CorePlugin plugin; + @Mock private CoreLogger logger; + @Mock private PluginConfig config; + @Mock private PlayerStateStore stateStore; + @Mock private SayanVanishHook sayanVanish; + @Mock private PremiumVanish premiumVanish; + @Mock private CorePlayer player; + + private final UUID playerUuid = UUID.randomUUID(); + + @BeforeEach + void setup() { + when(plugin.getCoreLogger()).thenReturn(logger); + when(player.getUniqueId()).thenReturn(playerUuid); + when(player.getName()).thenReturn("TestPlayer"); + + // Default: player not toggled silent, no vanish integrations active + when(stateStore.getSilentState(player)).thenReturn(false); + when(config.isSVTreatVanishedPlayersAsSilent()).thenReturn(false); + when(config.isPVTreatVanishedPlayersAsSilent()).thenReturn(false); + when(config.isPVTreatVanishedOnJoin()).thenReturn(false); + when(player.isPremiumVanishHidden()).thenReturn(false); + when(player.hasPermission("pv.joinvanished")).thenReturn(false); + } + + // ----------------------------------------------------------------------- + // isSilent(player) convenience overload + // ----------------------------------------------------------------------- + + @Test + void isSilent_noConditionsMet_returnsFalse() { + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, null, null); + assertFalse(checker.isSilent(player)); + } + + // ----------------------------------------------------------------------- + // Silent toggle state + // ----------------------------------------------------------------------- + + @Test + void isSilent_toggledSilent_returnsTrue() { + when(stateStore.getSilentState(player)).thenReturn(true); + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, null, null); + assertTrue(checker.isSilent(player)); + } + + @Test + void isSilent_toggleNotSilent_returnsFalse() { + when(stateStore.getSilentState(player)).thenReturn(false); + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, null, null); + assertFalse(checker.isSilent(player)); + } + + // ----------------------------------------------------------------------- + // SayanVanish integration + // ----------------------------------------------------------------------- + + @Test + void isSilent_sayanVanishPresent_treatSilentEnabled_playerVanished_returnsTrue() { + when(config.isSVTreatVanishedPlayersAsSilent()).thenReturn(true); + when(sayanVanish.isVanished(player)).thenReturn(true); + + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, sayanVanish, null); + assertTrue(checker.isSilent(player)); + } + + @Test + void isSilent_sayanVanishPresent_treatSilentEnabled_playerNotVanished_returnsFalse() { + when(config.isSVTreatVanishedPlayersAsSilent()).thenReturn(true); + when(sayanVanish.isVanished(player)).thenReturn(false); + + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, sayanVanish, null); + assertFalse(checker.isSilent(player)); + } + + @Test + void isSilent_sayanVanishPresent_treatSilentDisabled_playerVanished_returnsFalse() { + when(config.isSVTreatVanishedPlayersAsSilent()).thenReturn(false); + when(sayanVanish.isVanished(player)).thenReturn(true); + + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, sayanVanish, null); + assertFalse(checker.isSilent(player)); + } + + @Test + void isSilent_sayanVanishNull_vanishedFlagIrrelevant_returnsFalse() { + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, null, null); + assertFalse(checker.isSilent(player)); + } + + // ----------------------------------------------------------------------- + // PremiumVanish integration -- vanished via API + // ----------------------------------------------------------------------- + + @Test + void isSilent_pvPresent_treatSilentEnabled_playerVanishedByAPI_returnsTrue() { + when(config.isPVTreatVanishedPlayersAsSilent()).thenReturn(true); + when(premiumVanish.isVanished(playerUuid)).thenReturn(true); + + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, null, premiumVanish); + assertTrue(checker.isSilent(player)); + } + + @Test + void isSilent_pvPresent_treatSilentEnabled_playerNotVanished_returnsFalse() { + when(config.isPVTreatVanishedPlayersAsSilent()).thenReturn(true); + when(premiumVanish.isVanished(playerUuid)).thenReturn(false); + when(player.isPremiumVanishHidden()).thenReturn(false); + + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, null, premiumVanish); + assertFalse(checker.isSilent(player)); + } + + @Test + void isSilent_pvPresent_treatSilentEnabled_playerHiddenFlag_returnsTrue() { + when(config.isPVTreatVanishedPlayersAsSilent()).thenReturn(true); + when(premiumVanish.isVanished(playerUuid)).thenReturn(false); + when(player.isPremiumVanishHidden()).thenReturn(true); // hidden flag set from previous join + + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, null, premiumVanish); + assertTrue(checker.isSilent(player)); + } + + @Test + void isSilent_pvPresent_treatSilentDisabled_playerVanished_returnsFalse() { + when(config.isPVTreatVanishedPlayersAsSilent()).thenReturn(false); + when(premiumVanish.isVanished(playerUuid)).thenReturn(true); + + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, null, premiumVanish); + assertFalse(checker.isSilent(player)); + } + + // ----------------------------------------------------------------------- + // PremiumVanish -- TreatVanishedOnJoin + // ----------------------------------------------------------------------- + + @Test + void isSilent_pvTreatVanishedOnJoin_playerHasPerm_setsHiddenAndReturnsSilent() { + when(config.isPVTreatVanishedOnJoin()).thenReturn(true); + when(player.hasPermission("pv.joinvanished")).thenReturn(true); + when(config.isPVTreatVanishedPlayersAsSilent()).thenReturn(true); + // After setPremiumVanishHidden(true), player.isPremiumVanishHidden() should return true. + // We model this with a doAnswer or just verify the call was made. + when(premiumVanish.isVanished(playerUuid)).thenReturn(false); + when(player.isPremiumVanishHidden()).thenReturn(false); + + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, null, premiumVanish); + // joining=true triggers the PVTreatVanishedOnJoin path + checker.isSilent(player, false, true); + + // Verify that setPremiumVanishHidden(true) was invoked + verify(player).setPremiumVanishHidden(true); + } + + @Test + void isSilent_pvTreatVanishedOnJoin_playerNoPerm_doesNotSetHidden() { + when(config.isPVTreatVanishedOnJoin()).thenReturn(true); + when(player.hasPermission("pv.joinvanished")).thenReturn(false); + + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, null, premiumVanish); + checker.isSilent(player, false, true); + + verify(player, never()).setPremiumVanishHidden(true); + } + + @Test + void isSilent_pvTreatVanishedOnJoin_joiningFalse_doesNotSetHidden() { + when(config.isPVTreatVanishedOnJoin()).thenReturn(true); + when(player.hasPermission("pv.joinvanished")).thenReturn(true); + + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, null, premiumVanish); + // joining=false => the TreatVanishedOnJoin branch should be skipped + checker.isSilent(player, false, false); + + verify(player, never()).setPremiumVanishHidden(true); + } + + // ----------------------------------------------------------------------- + // Multiple conditions -- OR semantics + // ----------------------------------------------------------------------- + + @Test + void isSilent_multipleConditionsAnyTrue_returnsTrue() { + // toggle=false, SV vanished=false, PV vanished=true + when(config.isPVTreatVanishedPlayersAsSilent()).thenReturn(true); + when(premiumVanish.isVanished(playerUuid)).thenReturn(true); + when(config.isSVTreatVanishedPlayersAsSilent()).thenReturn(false); + when(sayanVanish.isVanished(player)).thenReturn(false); + + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, sayanVanish, premiumVanish); + assertTrue(checker.isSilent(player)); + } + + @Test + void isSilent_allConditionsFalse_returnsFalse() { + when(config.isSVTreatVanishedPlayersAsSilent()).thenReturn(false); + when(config.isPVTreatVanishedPlayersAsSilent()).thenReturn(false); + when(sayanVanish.isVanished(player)).thenReturn(true); // vanished but integration disabled + + SilenceChecker checker = new SilenceChecker(plugin, config, stateStore, sayanVanish, premiumVanish); + assertFalse(checker.isSilent(player)); + } +} diff --git a/src/test/java/xyz/earthcow/networkjoinmessages/common/storage/H2PlayerJoinTrackerTest.java b/src/test/java/xyz/earthcow/networkjoinmessages/common/storage/H2PlayerJoinTrackerTest.java index 17f6fbd..95b5501 100644 --- a/src/test/java/xyz/earthcow/networkjoinmessages/common/storage/H2PlayerJoinTrackerTest.java +++ b/src/test/java/xyz/earthcow/networkjoinmessages/common/storage/H2PlayerJoinTrackerTest.java @@ -3,29 +3,24 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; import org.mockito.Mockito; import xyz.earthcow.networkjoinmessages.common.abstraction.CoreLogger; import java.io.File; import java.nio.file.Files; -import java.nio.file.Path; +import java.util.Map; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; class H2PlayerJoinTrackerTest { - @TempDir - private Path tempDir; - private File tempDbFile; private H2PlayerJoinTracker tracker; @BeforeEach - void setUp() throws Exception { - tempDbFile = tempDir.resolve("joined").toFile(); + void setup() throws Exception { + tempDbFile = Files.createTempFile("join-tracker-h2-test", ".mv.db").toFile(); CoreLogger logger = Mockito.mock(CoreLogger.class); tracker = new H2PlayerJoinTracker(logger, tempDbFile.getAbsolutePath()); } @@ -36,83 +31,154 @@ void tearDown() throws Exception { if (tempDbFile.exists()) tempDbFile.delete(); } + // ----------------------------------------------------------------------- + // hasJoined -- baseline + // ----------------------------------------------------------------------- + + @Test + void hasJoined_unknownUuid_returnsFalse() { + assertFalse(tracker.hasJoined(UUID.randomUUID())); + } + + @Test + void hasJoined_afterMarkAsJoined_returnsTrue() { + UUID uuid = UUID.randomUUID(); + tracker.markAsJoined(uuid, "Notch"); + assertTrue(tracker.hasJoined(uuid)); + } + + // ----------------------------------------------------------------------- + // markAsJoined -- idempotency (MERGE semantics) + // ----------------------------------------------------------------------- + @Test - void testNewUUIDHasNotJoined() { + void markAsJoined_calledTwice_noException() { UUID uuid = UUID.randomUUID(); - assertFalse(tracker.hasJoined(uuid), "New UUID should not be marked as joined"); + assertDoesNotThrow(() -> { + tracker.markAsJoined(uuid, "Notch"); + tracker.markAsJoined(uuid, "Notch"); + }); } @Test - void testMarkAsJoined() { + void markAsJoined_calledTwiceSameUuid_appearsOnceInExport() { UUID uuid = UUID.randomUUID(); - assertFalse(tracker.hasJoined(uuid)); - tracker.markAsJoined(uuid, "Player 1"); - assertTrue(tracker.hasJoined(uuid), "UUID should be marked as joined after insert"); + tracker.markAsJoined(uuid, "Notch"); + tracker.markAsJoined(uuid, "NotchRenamed"); + + Map exported = tracker.exportAll(); + assertEquals(1, exported.size(), "MERGE must not create duplicate rows"); } @Test - void testDoubleInsert() { + void markAsJoined_calledTwiceNewName_nameIsUpdated() { UUID uuid = UUID.randomUUID(); - tracker.markAsJoined(uuid, "Player 2"); - tracker.markAsJoined(uuid, "Player 2"); // Should not throw any exceptions - assertTrue(tracker.hasJoined(uuid)); + tracker.markAsJoined(uuid, "OldName"); + tracker.markAsJoined(uuid, "NewName"); + + assertEquals("NewName", tracker.exportAll().get(uuid), + "MERGE should update the player name on second call"); } + // ----------------------------------------------------------------------- + // markAsJoined -- multiple distinct players + // ----------------------------------------------------------------------- + @Test - void testMultipleUUIDs() { + void markAsJoined_multipleDistinctPlayers_allTracked() { UUID uuid1 = UUID.randomUUID(); UUID uuid2 = UUID.randomUUID(); - tracker.markAsJoined(uuid1, "Player 3"); + UUID uuid3 = UUID.randomUUID(); + tracker.markAsJoined(uuid1, "Alpha"); + tracker.markAsJoined(uuid2, "Beta"); + tracker.markAsJoined(uuid3, "Gamma"); + assertTrue(tracker.hasJoined(uuid1)); - assertFalse(tracker.hasJoined(uuid2)); + assertTrue(tracker.hasJoined(uuid2)); + assertTrue(tracker.hasJoined(uuid3)); } - // --- User cache importer tests + // ----------------------------------------------------------------------- + // exportAll + // ----------------------------------------------------------------------- @Test - void addUsersFromUserCache_Success() throws Exception { - // Arrange - Path testCache = tempDir.resolve("usercache.json"); - String json = """ - [ - {"name": "Notch", "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5"}, - {"name": "jeb_", "uuid": "853c80ef-3c37-49fd-aa49-938b674adae6"} - ] - """; - Files.writeString(testCache, json); - - UUID randomUUID = UUID.randomUUID(); - - tracker.markAsJoined(randomUUID, "Player 1"); - - boolean result = tracker.addUsersFromUserCache(testCache.toString()); - - assertTrue(result); - assertTrue(tracker.hasJoined(randomUUID)); - assertTrue(tracker.hasJoined(UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"))); - assertTrue(tracker.hasJoined(UUID.fromString("853c80ef-3c37-49fd-aa49-938b674adae6"))); + void exportAll_emptyDatabase_returnsEmptyMap() { + assertTrue(tracker.exportAll().isEmpty()); } @Test - void addUsersFromUserCache_FileNotFound() { - assertFalse(tracker.addUsersFromUserCache("/nonexistent/path")); + void exportAll_returnsAllRegisteredPlayers() { + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + tracker.markAsJoined(uuid1, "Alpha"); + tracker.markAsJoined(uuid2, "Beta"); + + Map exported = tracker.exportAll(); + assertEquals(2, exported.size()); + assertEquals("Alpha", exported.get(uuid1)); + assertEquals("Beta", exported.get(uuid2)); } @Test - void addUsersFromUserCache_InvalidJson() throws Exception { - Path testCache = tempDir.resolve("usercache.json"); - Files.writeString(testCache, "INVALID_JSON"); + void exportAll_capturesPlayerNames() { + UUID uuid = UUID.randomUUID(); + tracker.markAsJoined(uuid, "Notch"); + + assertEquals("Notch", tracker.exportAll().get(uuid)); + } + + // ----------------------------------------------------------------------- + // UUID string representation round-trip + // ----------------------------------------------------------------------- - assertFalse(tracker.addUsersFromUserCache(testCache.toString())); + @Test + void uuidRoundTrip_dashFormattedUuid_roundTripsCorrectly() { + UUID uuid = UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"); + tracker.markAsJoined(uuid, "Notch"); + assertTrue(tracker.hasJoined(uuid)); + assertEquals("Notch", tracker.exportAll().get(uuid)); } + // ----------------------------------------------------------------------- + // Thread safety -- concurrent writes + // ----------------------------------------------------------------------- + @Test - void addUsersFromUserCache_EmptyArray() throws Exception { - Path testCache = tempDir.resolve("usercache.json"); - Files.writeString(testCache, "[]"); + void markAsJoined_concurrentCalls_noDataCorruption() throws Exception { + int threadCount = 20; + UUID[] uuids = new UUID[threadCount]; + for (int i = 0; i < threadCount; i++) uuids[i] = UUID.randomUUID(); + + Thread[] threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + final int idx = i; + threads[i] = new Thread(() -> tracker.markAsJoined(uuids[idx], "Player" + idx)); + } + for (Thread t : threads) t.start(); + for (Thread t : threads) t.join(); + + for (UUID uuid : uuids) { + assertTrue(tracker.hasJoined(uuid), + "All UUIDs should be tracked after concurrent inserts"); + } + } - boolean result = tracker.addUsersFromUserCache(testCache.toString()); + // ----------------------------------------------------------------------- + // Large batch + // ----------------------------------------------------------------------- - assertTrue(result); // Should still be successful but no users added + @Test + void markAsJoined_largeNumberOfPlayers_allTracked() { + int count = 200; + UUID[] uuids = new UUID[count]; + for (int i = 0; i < count; i++) { + uuids[i] = UUID.randomUUID(); + tracker.markAsJoined(uuids[i], "Player" + i); + } + assertEquals(count, tracker.exportAll().size()); + for (UUID uuid : uuids) { + assertTrue(tracker.hasJoined(uuid)); + } } } diff --git a/src/test/java/xyz/earthcow/networkjoinmessages/common/storage/TextPlayerJoinTrackerTest.java b/src/test/java/xyz/earthcow/networkjoinmessages/common/storage/TextPlayerJoinTrackerTest.java new file mode 100644 index 0000000..f769c1a --- /dev/null +++ b/src/test/java/xyz/earthcow/networkjoinmessages/common/storage/TextPlayerJoinTrackerTest.java @@ -0,0 +1,250 @@ +package xyz.earthcow.networkjoinmessages.common.storage; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import xyz.earthcow.networkjoinmessages.common.abstraction.CoreLogger; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class TextPlayerJoinTrackerTest { + + private Path tempFile; + private CoreLogger logger; + + @BeforeEach + void setup() throws Exception { + tempFile = Files.createTempFile("join-tracker-test", ".txt"); + // Remove file so the tracker creates its own header + Files.deleteIfExists(tempFile); + logger = Mockito.mock(CoreLogger.class); + } + + @AfterEach + void tearDown() throws Exception { + Files.deleteIfExists(tempFile); + } + + // ----------------------------------------------------------------------- + // Initialization -- file creation + // ----------------------------------------------------------------------- + + @Test + void init_fileDoesNotExist_createsFile() throws Exception { + new TextPlayerJoinTracker(logger, tempFile); + assertTrue(Files.exists(tempFile), "Tracker should create the file if absent"); + } + + @Test + void init_existingFile_doesNotThrow() throws Exception { + Files.writeString(tempFile, "# Comment\n"); + assertDoesNotThrow(() -> new TextPlayerJoinTracker(logger, tempFile)); + } + + // ----------------------------------------------------------------------- + // hasJoined -- fresh tracker + // ----------------------------------------------------------------------- + + @Test + void hasJoined_unknownUuid_returnsFalse() throws Exception { + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + assertFalse(tracker.hasJoined(UUID.randomUUID())); + } + + @Test + void hasJoined_afterMarkAsJoined_returnsTrue() throws Exception { + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + UUID uuid = UUID.randomUUID(); + tracker.markAsJoined(uuid, "Notch"); + assertTrue(tracker.hasJoined(uuid)); + } + + // ----------------------------------------------------------------------- + // markAsJoined -- idempotency + // ----------------------------------------------------------------------- + + @Test + void markAsJoined_calledTwice_noException() throws Exception { + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + UUID uuid = UUID.randomUUID(); + assertDoesNotThrow(() -> { + tracker.markAsJoined(uuid, "Notch"); + tracker.markAsJoined(uuid, "Notch"); + }); + } + + @Test + void markAsJoined_calledTwice_appearsOnceInExport() throws Exception { + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + UUID uuid = UUID.randomUUID(); + tracker.markAsJoined(uuid, "Notch"); + tracker.markAsJoined(uuid, "Notch"); // duplicate call + assertEquals(1, tracker.exportAll().size()); + } + + // ----------------------------------------------------------------------- + // markAsJoined -- multiple players + // ----------------------------------------------------------------------- + + @Test + void markAsJoined_multipleDistinctPlayers_allTracked() throws Exception { + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + tracker.markAsJoined(uuid1, "Alpha"); + tracker.markAsJoined(uuid2, "Beta"); + + assertTrue(tracker.hasJoined(uuid1)); + assertTrue(tracker.hasJoined(uuid2)); + } + + // ----------------------------------------------------------------------- + // Persistence -- reload from file + // ----------------------------------------------------------------------- + + @Test + void markAsJoined_persistsToFile() throws Exception { + UUID uuid = UUID.randomUUID(); + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + tracker.markAsJoined(uuid, "Notch"); + + // Read a fresh tracker from the same file + TextPlayerJoinTracker reloaded = new TextPlayerJoinTracker(logger, tempFile); + assertTrue(reloaded.hasJoined(uuid), + "UUID should be persisted and visible after reload"); + } + + @Test + void markAsJoined_multipleEntries_allPersist() throws Exception { + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + tracker.markAsJoined(uuid1, "Alpha"); + tracker.markAsJoined(uuid2, "Beta"); + + TextPlayerJoinTracker reloaded = new TextPlayerJoinTracker(logger, tempFile); + assertTrue(reloaded.hasJoined(uuid1)); + assertTrue(reloaded.hasJoined(uuid2)); + } + + // ----------------------------------------------------------------------- + // File format -- reading existing file content + // ----------------------------------------------------------------------- + + @Test + void init_parsesExistingUuidWithName() throws Exception { + UUID uuid = UUID.randomUUID(); + Files.writeString(tempFile, "# Header comment\n" + uuid + ":Notch\n"); + + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + assertTrue(tracker.hasJoined(uuid)); + } + + @Test + void init_parsesExistingUuidWithoutName() throws Exception { + UUID uuid = UUID.randomUUID(); + Files.writeString(tempFile, uuid + "\n"); + + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + assertTrue(tracker.hasJoined(uuid)); + } + + @Test + void init_skipsCommentLines() throws Exception { + Files.writeString(tempFile, "# This is a comment\n# Another comment\n"); + // Should not throw or produce any entries + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + assertTrue(tracker.exportAll().isEmpty()); + } + + @Test + void init_skipsBlankLines() throws Exception { + UUID uuid = UUID.randomUUID(); + Files.writeString(tempFile, "\n\n" + uuid + ":Player\n\n"); + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + assertEquals(1, tracker.exportAll().size()); + } + + @Test + void init_skipsInvalidUuidLines_logsInfo() throws Exception { + Files.writeString(tempFile, "not-a-valid-uuid:PlayerName\n"); + assertDoesNotThrow(() -> new TextPlayerJoinTracker(logger, tempFile)); + Mockito.verify(logger).info(Mockito.contains("Skipping")); + } + + @Test + void init_duplicateUuidInFile_onlyFirstEntryIsKept() throws Exception { + UUID uuid = UUID.randomUUID(); + Files.writeString(tempFile, uuid + ":FirstName\n" + uuid + ":SecondName\n"); + + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + // putIfAbsent semantics: first entry wins + assertEquals("FirstName", tracker.exportAll().get(uuid)); + } + + // ----------------------------------------------------------------------- + // exportAll + // ----------------------------------------------------------------------- + + @Test + void exportAll_emptyTracker_returnsEmptyMap() throws Exception { + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + assertTrue(tracker.exportAll().isEmpty()); + } + + @Test + void exportAll_returnsAllRegisteredPlayers() throws Exception { + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + tracker.markAsJoined(uuid1, "Alpha"); + tracker.markAsJoined(uuid2, "Beta"); + + Map exported = tracker.exportAll(); + assertEquals(2, exported.size()); + assertEquals("Alpha", exported.get(uuid1)); + assertEquals("Beta", exported.get(uuid2)); + } + + @Test + void exportAll_returnsUnmodifiableView() throws Exception { + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + tracker.markAsJoined(UUID.randomUUID(), "Player"); + + Map exported = tracker.exportAll(); + assertThrows(UnsupportedOperationException.class, + () -> exported.put(UUID.randomUUID(), "Injected"), + "exportAll should return an unmodifiable view"); + } + + // ----------------------------------------------------------------------- + // Thread safety -- concurrent access + // ----------------------------------------------------------------------- + + @Test + void markAsJoined_concurrentCalls_noDataCorruption() throws Exception { + TextPlayerJoinTracker tracker = new TextPlayerJoinTracker(logger, tempFile); + int threadCount = 20; + UUID[] uuids = new UUID[threadCount]; + for (int i = 0; i < threadCount; i++) uuids[i] = UUID.randomUUID(); + + Thread[] threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + final int idx = i; + threads[i] = new Thread(() -> tracker.markAsJoined(uuids[idx], "Player" + idx)); + } + for (Thread t : threads) t.start(); + for (Thread t : threads) t.join(); + + for (UUID uuid : uuids) { + assertTrue(tracker.hasJoined(uuid), + "All UUIDs should be tracked after concurrent inserts"); + } + } +} diff --git a/src/test/java/xyz/earthcow/networkjoinmessages/common/util/LegacyColorTranslatorTest.java b/src/test/java/xyz/earthcow/networkjoinmessages/common/util/LegacyColorTranslatorTest.java new file mode 100644 index 0000000..ba2f0d3 --- /dev/null +++ b/src/test/java/xyz/earthcow/networkjoinmessages/common/util/LegacyColorTranslatorTest.java @@ -0,0 +1,196 @@ +package xyz.earthcow.networkjoinmessages.common.util; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link LegacyColorTranslator}. + * This class is pure logic with no dependencies, so no mocking is required. + */ +class LegacyColorTranslatorTest { + + // ----------------------------------------------------------------------- + // translate() -- basic passthrough + // ----------------------------------------------------------------------- + + @Test + void translate_plainTextIsUnchanged() { + assertEquals("Hello world", LegacyColorTranslator.translate("Hello world")); + } + + @Test + void translate_emptyStringIsUnchanged() { + assertEquals("", LegacyColorTranslator.translate("")); + } + + // ----------------------------------------------------------------------- + // translate() -- section-sign to ampersand normalisation + // ----------------------------------------------------------------------- + + @Test + void translate_sectionSignIsConvertedToAmpersand() { + // §c should resolve to the same MiniMessage tag as &c + String viaSectionSign = LegacyColorTranslator.translate("§cRed"); + String viaAmpersand = LegacyColorTranslator.translate("&cRed"); + assertEquals(viaAmpersand, viaSectionSign, + "Section-sign and ampersand codes must produce identical output"); + } + + // ----------------------------------------------------------------------- + // translate() -- named color codes -> MiniMessage hex tags + // ----------------------------------------------------------------------- + + @ParameterizedTest(name = "code {0} -> MiniMessage color tag") + @ValueSource(strings = {"&0","&1","&2","&3","&4","&5","&6","&7", + "&8","&9","&a","&b","&c","&d","&e","&f"}) + void translate_namedColorCodesProduceMiniMessageColorTags(String code) { + String result = LegacyColorTranslator.translate(code + "Text"); + assertTrue(result.startsWith("<#"), + "Named color code " + code + " should be converted to a hex MiniMessage tag, got: " + result); + assertTrue(result.endsWith(">Text"), + "Translated output should preserve trailing text, got: " + result); + } + + // ----------------------------------------------------------------------- + // translate() -- formatting codes + // ----------------------------------------------------------------------- + + @ParameterizedTest(name = "formatting code {0} -> correct MiniMessage tag") + @CsvSource({ + "&k, ", + "&l, ", + "&m, ", + "&n, ", + "&o, ", + "&r, " + }) + void translate_formattingCodesProduceCorrectTags(String code, String expectedTag) { + String result = LegacyColorTranslator.translate(code); + assertTrue(result.contains(expectedTag), + "Code " + code + " should produce " + expectedTag + " but got: " + result); + } + + // ----------------------------------------------------------------------- + // translate() -- inline hex shorthand (&#RRGGBB) + // ----------------------------------------------------------------------- + + @Test + void translate_inlineHexShorthandIsConverted() { + String result = LegacyColorTranslator.translate("&#FF0000Red"); + assertEquals("<#FF0000>Red", result); + } + + @Test + void translate_inlineHexShorthandLowercaseIsConverted() { + String result = LegacyColorTranslator.translate("&#ff0000Red"); + assertEquals("<#ff0000>Red", result); + } + + @Test + void translate_multipleInlineHexCodesInOneString() { + String result = LegacyColorTranslator.translate("&#FF0000Red �FF00Green"); + assertEquals("<#FF0000>Red <#00FF00>Green", result); + } + + @Test + void translate_inlineHexShorthandDoesNotMatchFiveDigits() { + // &#FFFFF should not be converted -- only exactly six hex digits + String input = "&#FFFFFShouldBeIgnored"; + String result = LegacyColorTranslator.translate(input); + // The six-digit match is "FFFFF" + next char, so check the regex does not greedily eat it + // The important thing: no <# tag should appear for a five-digit sequence + assertFalse(result.matches(".*<#[0-9a-fA-F]{5}>.*"), + "Five-digit hex should not be converted: " + result); + } + + // ----------------------------------------------------------------------- + // translate() -- Essentials hex (§x§r§r§g§g§b§b) + // ----------------------------------------------------------------------- + + @Test + void translate_essentialsHexIsConverted() { + // §x§f§b§6§3§f§5 => &#fb63f5 => <#fb63f5> + String result = LegacyColorTranslator.translate("§x§f§b§6§3§f§5Hello!"); + assertEquals("<#fb63f5>Hello!", result); + } + + @Test + void translate_essentialsHexUppercaseDigitsAreHandled() { + // §x§F§F§0§0§0§0 => &#FF0000 => <#FF0000> + String result = LegacyColorTranslator.translate("§x§F§F§0§0§0§0Hi"); + assertEquals("<#FF0000>Hi", result); + } + + @Test + void translate_essentialsHexMixedWithRegularText() { + String result = LegacyColorTranslator.translate("Before §x§f§f§0§0§0§0Red After"); + assertEquals("Before <#ff0000>Red After", result); + } + + // ----------------------------------------------------------------------- + // translate() -- literal newline placeholder + // ----------------------------------------------------------------------- + + @Test + void translate_literalBackslashNIsConvertedToNewlineTag() { + // The static map replaces the two-char sequence "\n" (backslash + n) + String result = LegacyColorTranslator.translate("Line1\\nLine2"); + assertTrue(result.contains(""), + "Literal '\\n' in config strings should become tag, got: " + result); + } + + // ----------------------------------------------------------------------- + // translate() -- compound strings + // ----------------------------------------------------------------------- + + @Test + void translate_compoundStringWithMultipleCodes() { + String result = LegacyColorTranslator.translate("&cRed &aGreen &bAqua &#FF0000Hex"); + // Must not contain any raw & codes after translation + assertFalse(result.contains("&c"), "Raw &c should have been translated"); + assertFalse(result.contains("&a"), "Raw &a should have been translated"); + assertFalse(result.contains("&b"), "Raw &b should have been translated"); + assertFalse(result.contains("&#FF0000"), "Raw &#FF0000 should have been translated"); + } + + @Test + void translate_allCodeTypesInSingleStringDoesNotThrow() { + assertDoesNotThrow(() -> + LegacyColorTranslator.translate( + "&cRed &aGreen &bAqua &#FF0000Hex §x§f§f§0§0§0§0EssHex &lBold &r" + ) + ); + } + + // ----------------------------------------------------------------------- + // ESSENTIALS_HEX_PATTERN (public field) + // ----------------------------------------------------------------------- + + @Test + void essentialsHexPattern_matchesValidEssentialsCode() { + Pattern p = LegacyColorTranslator.ESSENTIALS_HEX_PATTERN; + assertTrue(p.matcher("§x§f§b§6§3§f§5").find()); + } + + @Test + void essentialsHexPattern_doesNotMatchPlainText() { + assertFalse(LegacyColorTranslator.ESSENTIALS_HEX_PATTERN.matcher("Regular text").find()); + } + + @Test + void essentialsHexPattern_doesNotMatchIncompleteCode() { + // Only five digit segments instead of six + assertFalse(LegacyColorTranslator.ESSENTIALS_HEX_PATTERN.matcher("§x§f§b§6§3§f").find()); + } + + @Test + void essentialsHexPattern_matchesUppercaseDigits() { + assertTrue(LegacyColorTranslator.ESSENTIALS_HEX_PATTERN.matcher("§x§A§B§C§D§E§F").find()); + } +} diff --git a/src/test/java/xyz/earthcow/networkjoinmessages/common/util/PremiumVanishLevelUtilTest.java b/src/test/java/xyz/earthcow/networkjoinmessages/common/util/PremiumVanishLevelUtilTest.java new file mode 100644 index 0000000..c244986 --- /dev/null +++ b/src/test/java/xyz/earthcow/networkjoinmessages/common/util/PremiumVanishLevelUtilTest.java @@ -0,0 +1,143 @@ +package xyz.earthcow.networkjoinmessages.common.util; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlayer; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class PremiumVanishLevelUtilTest { + + @Mock private CorePlayer player; + + // Helper: configure player to have exactly one numbered use level + private void grantUseLevel(int level) { + // Base permission + when(player.hasPermission("pv.use")).thenReturn(level >= 1); + // All numbered levels + for (int i = 1; i <= 100; i++) { + when(player.hasPermission("pv.use.level" + i)).thenReturn(i <= level); + } + } + + private void grantSeeLevel(int level) { + when(player.hasPermission("pv.see")).thenReturn(level >= 1); + for (int i = 1; i <= 100; i++) { + when(player.hasPermission("pv.see.level" + i)).thenReturn(i <= level); + } + } + + // ----------------------------------------------------------------------- + // updateVanishLevels -- use level + // ----------------------------------------------------------------------- + + @Test + void updateVanishLevels_noPermissions_useLevelIsZero() { + when(player.hasPermission(anyString())).thenReturn(false); + PremiumVanishLevelUtil.updateVanishLevels(player); + verify(player).setPremiumVanishUseLevel(0); + } + + @Test + void updateVanishLevels_baseUsePerm_useLevelIsOne() { + when(player.hasPermission(anyString())).thenReturn(false); + when(player.hasPermission("pv.use")).thenReturn(true); + // No numbered levels + PremiumVanishLevelUtil.updateVanishLevels(player); + verify(player).setPremiumVanishUseLevel(1); + } + + @ParameterizedTest(name = "pv.use.level{0} granted => useLevel={0}") + @ValueSource(ints = {1, 2, 5, 10, 50, 99, 100}) + void updateVanishLevels_numberedUsePerm_useLevelMatchesHighest(int level) { + grantUseLevel(level); + PremiumVanishLevelUtil.updateVanishLevels(player); + verify(player).setPremiumVanishUseLevel(level); + } + + // ----------------------------------------------------------------------- + // updateVanishLevels -- see level + // ----------------------------------------------------------------------- + + @Test + void updateVanishLevels_noPermissions_seeLevelIsZero() { + when(player.hasPermission(anyString())).thenReturn(false); + PremiumVanishLevelUtil.updateVanishLevels(player); + verify(player).setPremiumVanishSeeLevel(0); + } + + @Test + void updateVanishLevels_baseSeePerm_seeLevelIsOne() { + when(player.hasPermission(anyString())).thenReturn(false); + when(player.hasPermission("pv.see")).thenReturn(true); + PremiumVanishLevelUtil.updateVanishLevels(player); + verify(player).setPremiumVanishSeeLevel(1); + } + + @ParameterizedTest(name = "pv.see.level{0} granted => seeLevel={0}") + @ValueSource(ints = {1, 3, 7, 25, 75, 100}) + void updateVanishLevels_numberedSeePerm_seeLevelMatchesHighest(int level) { + grantSeeLevel(level); + PremiumVanishLevelUtil.updateVanishLevels(player); + verify(player).setPremiumVanishSeeLevel(level); + } + + // ----------------------------------------------------------------------- + // updateVanishLevels -- both levels set in a single call + // ----------------------------------------------------------------------- + + @Test + void updateVanishLevels_bothLevelsSetAtOnce() { + grantUseLevel(3); + grantSeeLevel(7); + PremiumVanishLevelUtil.updateVanishLevels(player); + verify(player).setPremiumVanishUseLevel(3); + verify(player).setPremiumVanishSeeLevel(7); + } + + // ----------------------------------------------------------------------- + // updateVanishLevels -- highest level is truly the highest + // ----------------------------------------------------------------------- + + @Test + void updateVanishLevels_onlyHighestLevelGranted_returnsCorrectLevel() { + // Player has ONLY pv.use.level50 (no 1-49, no 51-100, no base pv.use) + when(player.hasPermission(anyString())).thenReturn(false); + when(player.hasPermission("pv.use.level50")).thenReturn(true); + + PremiumVanishLevelUtil.updateVanishLevels(player); + verify(player).setPremiumVanishUseLevel(50); + } + + // ----------------------------------------------------------------------- + // updateVanishLevels -- no NPE when player has no permissions + // ----------------------------------------------------------------------- + + @Test + void updateVanishLevels_doesNotThrow() { + when(player.hasPermission(anyString())).thenReturn(false); + assertDoesNotThrow(() -> PremiumVanishLevelUtil.updateVanishLevels(player)); + } + + // ----------------------------------------------------------------------- + // Boundary -- level 100 is the maximum + // ----------------------------------------------------------------------- + + @Test + void updateVanishLevels_levelCappedAt100() { + grantUseLevel(100); + PremiumVanishLevelUtil.updateVanishLevels(player); + verify(player).setPremiumVanishUseLevel(100); + // No call should be made for level 101 + verify(player, never()).hasPermission("pv.use.level101"); + } +}