Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,19 @@
</exclusion>
</exclusions>
</dependency>
<!-- RPCharacters is softdepend; DiscordGateService uses reflection at runtime. -->
<!-- RPCharacters is optional; DiscordGateService uses reflection at runtime. -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<repositories>
Expand Down Expand Up @@ -74,6 +86,11 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
</plugins>
</build>
</project>
4 changes: 2 additions & 2 deletions src/main/java/net/tfminecraft/TFMCWeb/TFMCWeb.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public void onEnable() {

gateService = new DiscordGateService(this, linkCache);
if (!gateService.isRpcAvailable()) {
getLogger().warning(
"RPCharacters not found — Discord Survival gate disabled; link/HTTP still active."
getLogger().info(
"Discord Survival gate waiting for RPCharacters; availability is checked when used."
);
}

Expand Down
26 changes: 18 additions & 8 deletions src/main/java/net/tfminecraft/TFMCWeb/gate/DiscordGateService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,39 @@
import net.tfminecraft.TFMCWeb.cache.LinkCache;

/**
* Applies Survival Discord gate via RPCharacters (soft-depend, reflection).
* Applies Survival Discord gate via RPCharacters (optional, reflection).
*/
public final class DiscordGateService {

private final JavaPlugin plugin;
private final LinkCache linkCache;
private final boolean rpcAvailable;
private Plugin resolvedRpc;
private Method setGateUuid;
private Method setGatePlayer;

public DiscordGateService(JavaPlugin plugin, LinkCache linkCache) {
this.plugin = plugin;
this.linkCache = linkCache;
this.rpcAvailable = resolveRpc();
}

private boolean resolveRpc() {
Plugin rpc = Bukkit.getPluginManager().getPlugin("RPCharacters");
if (rpc == null || !rpc.isEnabled()) {
resolvedRpc = null;
setGateUuid = null;
setGatePlayer = null;
return false;
}
if (rpc == resolvedRpc) {
return setGateUuid != null && setGatePlayer != null;
}
resolvedRpc = rpc;
setGateUuid = null;
setGatePlayer = null;
try {
Class<?> cls = Class.forName("net.tfminecraft.RPCharacters.RPCharacters");
// RPC depends on TFMCWeb, so resolve only once it has enabled, using
// its own class rather than TFMCWeb's plugin class loader.
Class<?> cls = rpc.getClass();
setGateUuid = cls.getMethod("setDiscordGate", UUID.class, boolean.class);
setGatePlayer = cls.getMethod("setDiscordGate", Player.class, boolean.class);
return true;
Expand All @@ -52,7 +62,7 @@ private boolean resolveRpc() {
}

public boolean isRpcAvailable() {
return rpcAvailable;
return resolveRpc();
}

/**
Expand All @@ -77,7 +87,7 @@ public void applyGate(Player player) {
}

public void applyGate(Player player, boolean eligible) {
if (player == null || !rpcAvailable) {
if (player == null || !isRpcAvailable()) {
return;
}
boolean required = player.getGameMode() == GameMode.SURVIVAL && !eligible;
Expand All @@ -86,7 +96,7 @@ public void applyGate(Player player, boolean eligible) {

/** Offline-safe: set UUID gate flag; RPC reevaluates if online. */
public void applyGate(UUID uuid, boolean eligible) {
if (uuid == null || !rpcAvailable) {
if (uuid == null || !isRpcAvailable()) {
return;
}
Player online = Bukkit.getPlayer(uuid);
Expand All @@ -99,7 +109,7 @@ public void applyGate(UUID uuid, boolean eligible) {
}

public void clearGate(UUID uuid) {
if (uuid == null || !rpcAvailable) {
if (uuid == null || !isRpcAvailable()) {
return;
}
Player online = Bukkit.getPlayer(uuid);
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ version: ${project.version}
author: TFMC

depend: [TLibs]
softdepend: [RPCharacters, Essentials]
# RPCharacters depends on TFMCWeb; its optional gate API is resolved at runtime.
softdepend: [Essentials]

commands:
linkdiscord:
Expand Down
146 changes: 146 additions & 0 deletions src/test/java/net/tfminecraft/TFMCWeb/gate/DiscordGateServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package net.tfminecraft.TFMCWeb.gate;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;

import java.util.UUID;
import java.util.logging.Logger;

import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;

import net.tfminecraft.TFMCWeb.cache.LinkCache;

class DiscordGateServiceTest {

private MockedStatic<Bukkit> bukkit;
private PluginManager plugins;
private RpcPlugin rpc;
private Player player;
private UUID uuid;
private LinkCache cache;
private DiscordGateService gate;

@BeforeEach
void setUp() {
bukkit = mockStatic(Bukkit.class);
plugins = mock(PluginManager.class);
bukkit.when(Bukkit::getPluginManager).thenReturn(plugins);
rpc = mock(RpcPlugin.class);
when(plugins.getPlugin("RPCharacters")).thenReturn(rpc);
JavaPlugin web = mock(JavaPlugin.class);
when(web.getLogger()).thenReturn(Logger.getAnonymousLogger());
player = mock(Player.class);
uuid = UUID.randomUUID();
when(player.getUniqueId()).thenReturn(uuid);
when(player.isOnline()).thenReturn(true);
when(player.getGameMode()).thenReturn(GameMode.SURVIVAL);
cache = new LinkCache();
RpcPlugin.lastUuid = null;
RpcPlugin.lastRequired = null;
// TFMCWeb starts while its dependent RPCharacters is still disabled.
gate = new DiscordGateService(web, cache);
}

@AfterEach
void tearDown() {
bukkit.close();
}

@Test
void freezesUnlinkedSurvivalPlayerAfterRpcEnablesLater() {
assertFalse(gate.isRpcAvailable());
gate.applyGate(player);
assertNull(RpcPlugin.lastRequired);

when(rpc.isEnabled()).thenReturn(true);
gate.applyGate(player);

assertEquals(uuid, RpcPlugin.lastUuid);
assertEquals(Boolean.TRUE, RpcPlugin.lastRequired);
assertTrue(gate.isRpcAvailable());
}

@Test
void releasesEligiblePlayersAndDoesNotFreezeOtherGameModes() {
when(rpc.isEnabled()).thenReturn(true);
cache.putLinked(uuid, "discord-id", "player");
gate.applyGate(player);
assertEquals(Boolean.FALSE, RpcPlugin.lastRequired);

cache.putUnlinked(uuid);
for (GameMode mode : GameMode.values()) {
when(player.getGameMode()).thenReturn(mode);
gate.applyGate(player);
assertEquals(mode == GameMode.SURVIVAL, RpcPlugin.lastRequired);
}
}

@Test
void appliesAndClearsOfflineGateAfterRpcEnablesLater() {
assertFalse(gate.isRpcAvailable());
when(rpc.isEnabled()).thenReturn(true);
gate.applyGate(uuid, false);
assertEquals(uuid, RpcPlugin.lastUuid);
assertEquals(Boolean.TRUE, RpcPlugin.lastRequired);

gate.clearGate(uuid);
assertEquals(Boolean.FALSE, RpcPlugin.lastRequired);
}

@Test
void clearGateAlsoResolvesRpcAfterLateEnable() {
when(rpc.isEnabled()).thenReturn(true);
gate.clearGate(uuid);
assertEquals(uuid, RpcPlugin.lastUuid);
assertEquals(Boolean.FALSE, RpcPlugin.lastRequired);
}

@Test
void toleratesMissingRpcAndStopsCallingDisabledRpc() {
when(plugins.getPlugin("RPCharacters")).thenReturn(null);
gate.applyGate(player);
assertFalse(gate.isRpcAvailable());
assertNull(RpcPlugin.lastRequired);

when(plugins.getPlugin("RPCharacters")).thenReturn(rpc);
when(rpc.isEnabled()).thenReturn(true);
assertTrue(gate.isRpcAvailable());
when(rpc.isEnabled()).thenReturn(false);
gate.applyGate(player);
gate.applyGate(uuid, false);
gate.clearGate(uuid);
assertFalse(gate.isRpcAvailable());
assertNull(RpcPlugin.lastRequired);

when(rpc.isEnabled()).thenReturn(true);
gate.applyGate(player);
assertEquals(Boolean.TRUE, RpcPlugin.lastRequired);
}

public static class RpcPlugin extends JavaPlugin {
static UUID lastUuid;
static Boolean lastRequired;

public static void setDiscordGate(UUID uuid, boolean required) {
lastUuid = uuid;
lastRequired = required;
}

public static void setDiscordGate(Player player, boolean required) {
setDiscordGate(player.getUniqueId(), required);
}
}
}