mirror of
https://github.com/BenCodez/VotingPlugin.git
synced 2026-11-04 17:10:03 -05:00
Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46a6e47a80 | ||
|
|
36bf96559e | ||
|
|
63c3c9aecf | ||
|
|
8acf825eaa | ||
|
|
e40bf57093 | ||
|
|
0c512eb8c1 | ||
|
|
de9852a974 | ||
|
|
ad0c58d945 | ||
|
|
dea8c3a8ba | ||
|
|
f79e4c5d36 | ||
|
|
ca591ed39c | ||
|
|
ccfe5f1cf1 | ||
|
|
2b59b83123 | ||
|
|
1fbc1a1939 | ||
|
|
c6a099fdb1 | ||
|
|
03bbdb4c93 | ||
|
|
4a2dae0e47 | ||
|
|
1ce14cc0ec | ||
|
|
e2f326de2a | ||
|
|
0518038d91 | ||
|
|
5fc7d5a67b | ||
|
|
f5b8c669a3 | ||
|
|
22ba18f481 | ||
|
|
1a19feebbb | ||
|
|
a39d2cf77f | ||
|
|
21204495bd | ||
|
|
b510322a09 | ||
|
|
7821ccaaff | ||
|
|
229c096086 | ||
|
|
b5c759570a | ||
|
|
7321ea5da4 | ||
|
|
832772f382 | ||
|
|
fda4b09909 | ||
|
|
6cbc180d7b | ||
|
|
51e2396f0d | ||
|
|
6a5c124ec3 | ||
|
|
54cffb5f95 | ||
|
|
a1905322ae | ||
|
|
fa39ec9b7c | ||
|
|
0be5deedfe | ||
|
|
9865b50938 | ||
|
|
65498873f7 | ||
|
|
4615be0eaa | ||
|
|
c2446c5151 |
No files matched your search
@@ -26,6 +26,8 @@ mvn -B -f VotingPlugin/pom.xml -Dtest=BackendControlConnectorProtocolTest,Contro
|
||||
CI runs `mvn -B -f VotingPlugin/pom.xml package`; see `.github/workflows/maven.yml`. Do not use the `dev` Maven profile in
|
||||
automation because it copies a JAR into a developer-specific server directory.
|
||||
|
||||
All project tests and test-only helpers belong under `VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/`, with package `com.bencodez.votingplugin.tests` or a meaningful subpackage. Keep paths and package declarations aligned, and update cross-test imports, resources, and build selectors when moving tests. Preserve package-phase artifact checks and test discovery. Do not widen production APIs to make tests accessible; use narrowly scoped test-only reflection for non-public seams. Platform API fixtures that must retain an external runtime class identity (such as `net.minecraft.network.chat.Component`) stay in that platform package and must not contain test cases.
|
||||
|
||||
Assign one owner to Maven validation in each worktree. Never run concurrent Maven commands in the same worktree, including focused tests alongside a clean/package build: compilation and cleanup share `target/` and can invalidate each other. Reviewers must inspect source only while that owner builds, or run validation in a separate worktree.
|
||||
|
||||
Keep the downloadable VotingPlugin JAR as small as practical. The 10 MiB size target is advisory:
|
||||
@@ -58,6 +60,10 @@ For compatibility-sensitive changes, add regression coverage that exercises the
|
||||
- `com.bencodez.votingplugin.core` and its subpackages are platform-independent. Classes there must not import Bukkit, Paper, Fabric, Forge, or NeoForge APIs. Keep loader-specific adapters outside this package and enforce the boundary with `CorePlatformIsolationTest`.
|
||||
- `proxy/VotingPluginProxy` and the Bungee/Velocity platform packages own proxy lifecycle and vote routing.
|
||||
- `listeners/` receives Bukkit-side vote/player events; `proxy/cache/` owns proxy pending-vote queues.
|
||||
- `commands/CommandLoader` captures `VoteCommandEnabled`, `VoteCommandName` and `LoadCommandAliases` at startup.
|
||||
Configuration reload must not change the registered player root or generated help/navigation. Preserve exact command
|
||||
ownership, admin roots/aliases and canonical `VotingPlugin.Commands.Vote.*` permissions when disabling or renaming;
|
||||
standalone player aliases retain their `vote...`/`v...` identities. See `docs/player-command-root.md`.
|
||||
- `votesites/` resolves configured service names. Be alert to the distinction between read-only resolution and paths that
|
||||
may auto-create a site.
|
||||
- `user/` owns player totals, points, streaks, last-vote values, and backend offline rewards.
|
||||
@@ -351,3 +357,7 @@ Control behavior in production.
|
||||
- After a MEX write, say exactly what changed and its sharing boundary: a local draft is checkout-only and nothing is shared; a canonical artifact is written to the working tree and requires commit/push to share.
|
||||
- Skill activation is not approval for canonical actions.
|
||||
<!-- mex-agent:skills:end -->
|
||||
|
||||
## Guided proxy session confirmation
|
||||
|
||||
Guided sessions never award votes or rewards. Proxy confirmation requires a bounded versioned session-fence reply matching the player, backend and random session nonce; keep the first fence for each source. New original proxy ingress assigns an opaque UUIDv8 before asynchronous handoff, with random source incarnation and monotonically increasing sequence. Preserve that canonical ID through every journal, retry, cache and multi-proxy hop; forwarding or replay must never mint new occurrence order. Do not compare proxy clocks with backend clocks or infer freshness from delivery/queue flags. New post-fence delayed/queued votes can confirm only through the existing credited pipeline; older, legacy, fake/test and unsupported-source occurrences remain unconfirmed without changing normal accounting. Unknown peers must preserve ordinary voting. No cache schema migration or new reward path.
|
||||
@@ -52,3 +52,7 @@ or disabling it; inspections gate disabled state immediately and report a newly
|
||||
```
|
||||
|
||||
`LATEST` resolves to the latest stable release.
|
||||
|
||||
## Guided voting session
|
||||
|
||||
Optional `/vote session` chat navigation tracks real accepted votes without changing existing menus or rewards. See [configuration, controls and timing limits](docs/guided-voting-session.md).
|
||||
@@ -382,7 +382,7 @@
|
||||
<dependency>
|
||||
<groupId>net.neoforged</groupId>
|
||||
<artifactId>mergetool</artifactId>
|
||||
<version>2.0.0</version>
|
||||
<version>2.0.7</version>
|
||||
<classifier>api</classifier>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
@@ -406,7 +406,7 @@
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>26.2-R0.1-SNAPSHOT</version>
|
||||
<version>26.3-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
@@ -475,7 +475,7 @@
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>7.5.3</version>
|
||||
<version>8.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>me.clip</groupId>
|
||||
@@ -525,7 +525,7 @@
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.12.2</version>
|
||||
<version>6.1.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -537,13 +537,13 @@
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.12.2</version>
|
||||
<version>6.1.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<version>5.23.0</version>
|
||||
<version>5.24.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
||||
@@ -71,11 +71,9 @@ import com.bencodez.votingplugin.broadcast.BroadcastSettings;
|
||||
import com.bencodez.votingplugin.proxy.BungeeMethod;
|
||||
import com.bencodez.votingplugin.commands.CommandLoader;
|
||||
import com.bencodez.votingplugin.commands.executers.CommandAdminVote;
|
||||
import com.bencodez.votingplugin.commands.executers.CommandVote;
|
||||
import com.bencodez.votingplugin.commands.gui.AdminGUI;
|
||||
import com.bencodez.votingplugin.commands.tabcompleter.AdminVoteTabCompleter;
|
||||
import com.bencodez.votingplugin.commands.tabcompleter.AliasCommandFilterListener;
|
||||
import com.bencodez.votingplugin.commands.tabcompleter.VoteTabCompleter;
|
||||
import com.bencodez.votingplugin.config.BungeeSettings;
|
||||
import com.bencodez.votingplugin.config.Config;
|
||||
import com.bencodez.votingplugin.config.ConfigVoteSites;
|
||||
@@ -87,6 +85,9 @@ import com.bencodez.votingplugin.cooldown.CoolDownCheck;
|
||||
import com.bencodez.votingplugin.control.BackendControlAutoEnrollment;
|
||||
import com.bencodez.votingplugin.control.BackendControlConnector;
|
||||
import com.bencodez.votingplugin.data.ServerData;
|
||||
import com.bencodez.votingplugin.maintenance.AutoCreateVoteSitesReminderService;
|
||||
import com.bencodez.votingplugin.maintenance.AutoCreateVoteSitesReminderService.Settings;
|
||||
import com.bencodez.votingplugin.core.maintenance.AutoCreateVoteSitesReminderState;
|
||||
import com.bencodez.votingplugin.discord.DiscordHandler;
|
||||
import com.bencodez.votingplugin.listeners.BlockBreak;
|
||||
import com.bencodez.votingplugin.integration.votifier.VotifierIntegration;
|
||||
@@ -155,6 +156,13 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class VotingPluginMain extends AdvancedCorePlugin {
|
||||
private final com.bencodez.votingplugin.session.GuidedVotingSessions guidedVotingSessions =
|
||||
new com.bencodez.votingplugin.session.GuidedVotingSessions(this);
|
||||
|
||||
public com.bencodez.votingplugin.session.GuidedVotingSessions getGuidedVotingSessions() {
|
||||
return guidedVotingSessions;
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
public static VotingPluginMain plugin;
|
||||
@@ -174,6 +182,9 @@ public class VotingPluginMain extends AdvancedCorePlugin {
|
||||
@Getter
|
||||
private BroadcastHandler broadcastHandler;
|
||||
|
||||
@Getter
|
||||
private com.bencodez.votingplugin.streaks.VoteStreaksMenu voteStreaksMenu;
|
||||
|
||||
@Getter
|
||||
private BackendProxyHandler backendProxyHandler;
|
||||
private ProcessedVoteCache backendProcessedVoteCache;
|
||||
@@ -213,6 +224,21 @@ public class VotingPluginMain extends AdvancedCorePlugin {
|
||||
@Getter
|
||||
private CommandLoader commandLoader;
|
||||
|
||||
/** Runtime command identity is captured at startup; configuration reload does not rename it. */
|
||||
public String getVoteCommandPrefix() {
|
||||
return "/" + (commandLoader == null ? "vote" : commandLoader.getVoteCommandName());
|
||||
}
|
||||
|
||||
public boolean isVoteCommandEnabled() {
|
||||
return commandLoader == null || commandLoader.isVoteCommandEnabled();
|
||||
}
|
||||
|
||||
/** Retarget the built-in invalid-command help hint, including existing Config.yml text. */
|
||||
public String formatVoteCommandMessage(String message) {
|
||||
return message.replace("%votecommand%", getVoteCommandPrefix())
|
||||
.replaceAll("(?i)/vote(?=\\s|$|[!.,])", getVoteCommandPrefix());
|
||||
}
|
||||
|
||||
@Getter
|
||||
private NameMCLikeCheckerTask nameMCLikeCheckerTask;
|
||||
|
||||
@@ -296,6 +322,8 @@ public class VotingPluginMain extends AdvancedCorePlugin {
|
||||
|
||||
@Getter
|
||||
private ScheduledExecutorService voteTimer;
|
||||
private volatile AutoCreateVoteSitesReminderService autoCreateVoteSitesSetupReminder;
|
||||
private ScheduledExecutorService autoCreateVoteSitesReminderWorker;
|
||||
|
||||
@Getter
|
||||
private UserManager votingPluginUserManager;
|
||||
@@ -722,6 +750,16 @@ public class VotingPluginMain extends AdvancedCorePlugin {
|
||||
voteTester = new VoteTester(plugin);
|
||||
|
||||
loadVoteTimer();
|
||||
BoundedScheduledExecutor setupReminderWorker = new BoundedScheduledExecutor(1, 4);
|
||||
setupReminderWorker.setThreadFactory(runnable -> {
|
||||
Thread worker = new Thread(runnable, "VotingPlugin-SiteSetupReminder");
|
||||
worker.setDaemon(true);
|
||||
return worker;
|
||||
});
|
||||
autoCreateVoteSitesReminderWorker = setupReminderWorker;
|
||||
autoCreateVoteSitesSetupReminder = new AutoCreateVoteSitesReminderService(getDataFolder().toPath(),
|
||||
setupReminderWorker, getLogger(), autoCreateVoteSitesReminderSettings());
|
||||
autoCreateVoteSitesSetupReminder.start();
|
||||
getVotingPluginUserManager().startSharedPointTransferRecovery();
|
||||
|
||||
if (bungeeSettings.isUseBungeecoord()) {
|
||||
@@ -1944,14 +1982,34 @@ public class VotingPluginMain extends AdvancedCorePlugin {
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
stopAutoCreateVoteSitesSetupReminder();
|
||||
if (offlineVoteOwnerHandoffs != null) offlineVoteOwnerHandoffs.close();
|
||||
try {
|
||||
guidedVotingSessions.clear();
|
||||
shutdownVoteReminders();
|
||||
} finally {
|
||||
super.onDisable();
|
||||
}
|
||||
}
|
||||
|
||||
private Settings autoCreateVoteSitesReminderSettings() {
|
||||
return new Settings(true, configFile.isAutoCreateVoteSites(),
|
||||
AutoCreateVoteSitesReminderState.DEFAULT_THRESHOLD_MILLIS,
|
||||
AutoCreateVoteSitesReminderState.DEFAULT_REPEAT_MILLIS);
|
||||
}
|
||||
|
||||
private void stopAutoCreateVoteSitesSetupReminder() {
|
||||
AutoCreateVoteSitesReminderService current = autoCreateVoteSitesSetupReminder;
|
||||
autoCreateVoteSitesSetupReminder = null;
|
||||
ScheduledExecutorService worker = autoCreateVoteSitesReminderWorker;
|
||||
autoCreateVoteSitesReminderWorker = null;
|
||||
try {
|
||||
if (current != null) current.closeAsync();
|
||||
} finally {
|
||||
if (worker != null) worker.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private void shutdownVoteReminders() {
|
||||
if (voteRemindersManager != null) {
|
||||
voteRemindersManager.shutdown();
|
||||
@@ -1961,6 +2019,8 @@ public class VotingPluginMain extends AdvancedCorePlugin {
|
||||
|
||||
@Override
|
||||
public void onUnLoad() {
|
||||
if (voteStreaksMenu != null) voteStreaksMenu.close();
|
||||
stopAutoCreateVoteSitesSetupReminder();
|
||||
placeholderPlayerPresence.clear();
|
||||
stopBackendHostedControlLifecycle();
|
||||
stopBackendControlConnectorLifecycle();
|
||||
@@ -2013,12 +2073,6 @@ public class VotingPluginMain extends AdvancedCorePlugin {
|
||||
commandLoader.loadCommands();
|
||||
commandLoader.loadAliases();
|
||||
|
||||
// /vote, /v
|
||||
getCommand("vote").setExecutor(new CommandVote(this));
|
||||
getCommand("vote").setTabCompleter(new VoteTabCompleter());
|
||||
// getCommand("v").setExecutor(new CommandVote(this));
|
||||
// getCommand("v").setTabCompleter(new VoteTabCompleter());
|
||||
|
||||
// /adminvote, /av
|
||||
getCommand("adminvote").setExecutor(new CommandAdminVote(this));
|
||||
getCommand("adminvote").setTabCompleter(new AdminVoteTabCompleter());
|
||||
@@ -2045,15 +2099,18 @@ public class VotingPluginMain extends AdvancedCorePlugin {
|
||||
* Register events.
|
||||
*/
|
||||
private void registerEvents() {
|
||||
voteStreaksMenu = new com.bencodez.votingplugin.streaks.VoteStreaksMenu(this);
|
||||
getServer().getPluginManager().registerEvents(voteStreaksMenu, this);
|
||||
PluginManager pm = getServer().getPluginManager();
|
||||
|
||||
pm.registerEvents(new PlayerJoinEvent(this), this);
|
||||
if (isVotifierLoaded()) {
|
||||
VotiferEvent votifierEvent = new VotiferEvent(this);
|
||||
votifierVoteOverflowQueue = new VotifierVoteOverflowQueue(this, votifierEvent::processVote);
|
||||
votifierVoteOverflowQueue = VotifierVoteOverflowQueue.withObservations(this, votifierEvent::processVote);
|
||||
pm.registerEvents(votifierEvent, this);
|
||||
}
|
||||
pm.registerEvents(new PlayerVoteListener(this), this);
|
||||
pm.registerEvents(guidedVotingSessions, this);
|
||||
pm.registerEvents(new PlayerPostVoteLoggerListener(this), this);
|
||||
pm.registerEvents(new PlayerSpecialRewardLoggerListener(this), this);
|
||||
pm.registerEvents(new VoteShopPurchaseLoggerListener(this), this);
|
||||
@@ -2100,15 +2157,20 @@ public class VotingPluginMain extends AdvancedCorePlugin {
|
||||
/** Captures Bukkit presence while the lifecycle caller owns platform access. */
|
||||
public void refreshPlaceholderPlayerPresence() {
|
||||
boolean onlineMode = getOptions().isOnlineMode();
|
||||
placeholderPlayerPresence.replace(Bukkit::getOnlinePlayers,
|
||||
player -> placeholderStorageUuid(player, onlineMode));
|
||||
placeholderPlayerPresence.refresh(Bukkit::getOnlinePlayers, (player, capture) ->
|
||||
com.bencodez.votingplugin.util.BukkitCompletionScheduler.run(this, player, () -> {
|
||||
if (!player.isOnline()) return;
|
||||
UUID storageUuid = placeholderPlayerPresence.storageUuid(player);
|
||||
capture.accept(storageUuid == null ? placeholderStorageUuid(player, onlineMode) : storageUuid);
|
||||
}, () -> { }, () -> { }));
|
||||
}
|
||||
|
||||
static UUID placeholderStorageUuid(Player player, boolean onlineMode) {
|
||||
/** Snapshot-only UUID resolution; call while owning the player, before storage work. */
|
||||
public static UUID placeholderStorageUuid(Player player, boolean onlineMode) {
|
||||
if (onlineMode) return player.getUniqueId();
|
||||
String cachedUuid = UuidLookup.getInstance().getCachedUUID(player.getName());
|
||||
String resolvedUuid = UuidLookup.getInstance().getUUIDWithoutStorage(player.getName());
|
||||
try {
|
||||
return UUID.fromString(cachedUuid);
|
||||
return UUID.fromString(resolvedUuid);
|
||||
} catch (IllegalArgumentException | NullPointerException ignored) {
|
||||
return player.getUniqueId();
|
||||
}
|
||||
@@ -2125,8 +2187,13 @@ public class VotingPluginMain extends AdvancedCorePlugin {
|
||||
boolean updateActiveBackendRuntime) {
|
||||
if (offlineVoteOwnerHandoffs != null) offlineVoteOwnerHandoffs.pause();
|
||||
try {
|
||||
guidedVotingSessions.clear();
|
||||
if (voteStreaksMenu != null) voteStreaksMenu.suspend();
|
||||
configFile.reloadData();
|
||||
configFile.loadValues();
|
||||
if (commandLoader != null) commandLoader.refreshGuidedSessionCommands();
|
||||
AutoCreateVoteSitesReminderService setupReminder = autoCreateVoteSitesSetupReminder;
|
||||
if (setupReminder != null) setupReminder.reload(autoCreateVoteSitesReminderSettings());
|
||||
|
||||
configVoteSites.reloadData();
|
||||
|
||||
@@ -2176,6 +2243,7 @@ public class VotingPluginMain extends AdvancedCorePlugin {
|
||||
|
||||
setUpdate(true);
|
||||
} finally {
|
||||
if (voteStreaksMenu != null) voteStreaksMenu.resume();
|
||||
if (offlineVoteOwnerHandoffs != null) offlineVoteOwnerHandoffs.resume();
|
||||
}
|
||||
}
|
||||
|
||||
+12
-3
@@ -57,7 +57,12 @@ public class BackendProxyMessageRouter {
|
||||
}
|
||||
|
||||
public void register(GlobalMessageHandler messages, BungeeMethod method) {
|
||||
this.messages = messages;
|
||||
this.messages = messages;
|
||||
messages.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_SESSION_FENCE_REPLY) {
|
||||
@Override public void onReceive(JsonEnvelope message) {
|
||||
plugin.getGuidedVotingSessions().proxyFence(message);
|
||||
}
|
||||
});
|
||||
messages.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE) {
|
||||
@Override public void onReceive(JsonEnvelope msg) { handleWireVote(msg); }
|
||||
});
|
||||
@@ -715,11 +720,15 @@ public class BackendProxyMessageRouter {
|
||||
user.cache();
|
||||
boolean wasOnline = vote.wasOnlineKnown ? vote.wasOnline : user.isOnline();
|
||||
boolean queuedDelivery = vote.queuedDeliveryKnown ? vote.queuedDelivery : vote.delayValidated;
|
||||
// Even an immediate proxy send can arrive after a guide opened on this backend.
|
||||
// Transport freshness flags cannot establish original occurrence order. The guide
|
||||
// separately validates the canonical ingress ID against its negotiated source fence.
|
||||
var event = user.bungeeVotePluginMessagingEvent(vote.service, vote.time, totals, !vote.manageTotals,
|
||||
wasOnline, vote.broadcast, vote.num, queuedDelivery, vote.delayValidationKnown,
|
||||
vote.queuedDeliveryKnown, voteId,
|
||||
VotingPluginWire.SUB_VOTE_ONLINE.equals(msg.getSubChannel()));
|
||||
if ((event == null || !event.isCancelled()) && plugin.getBungeeSettings().isPerServerPoints()) {
|
||||
VotingPluginWire.SUB_VOTE_ONLINE.equals(msg.getSubChannel()),
|
||||
true, vote.realVote);
|
||||
if ((event == null || !event.isCancelled()) && plugin.getBungeeSettings().isPerServerPoints()) {
|
||||
user.addPoints(plugin.getConfigFile().getPointsOnVote());
|
||||
}
|
||||
if (vote.service != null && !vote.service.isEmpty()) {
|
||||
|
||||
@@ -56,6 +56,7 @@ import com.bencodez.simpleapi.valuerequest.ValueRequest;
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
import com.bencodez.votingplugin.core.maintenance.MaintenanceConfirmation;
|
||||
import com.bencodez.votingplugin.commands.executers.CommandAliases;
|
||||
import com.bencodez.votingplugin.commands.executers.CommandVote;
|
||||
import com.bencodez.votingplugin.commands.gui.AdminGUI;
|
||||
import com.bencodez.votingplugin.commands.gui.admin.AdminVoteHelp;
|
||||
import com.bencodez.votingplugin.commands.gui.admin.AdminVotePerms;
|
||||
@@ -82,6 +83,7 @@ import com.bencodez.votingplugin.commands.gui.player.VoteTotal;
|
||||
import com.bencodez.votingplugin.commands.gui.player.VoteURL;
|
||||
import com.bencodez.votingplugin.commands.gui.player.VoteURLVoteSite;
|
||||
import com.bencodez.votingplugin.commands.tabcompleter.AliasesTabCompleter;
|
||||
import com.bencodez.votingplugin.commands.tabcompleter.VoteTabCompleter;
|
||||
import com.bencodez.votingplugin.events.PlayerVoteEvent;
|
||||
import com.bencodez.votingplugin.events.PlayerVoteSiteCoolDownEndEvent;
|
||||
import com.bencodez.votingplugin.presets.VoteSitePresetSetupHandler;
|
||||
@@ -102,6 +104,10 @@ import com.bencodez.votingplugin.votesites.VoteSite;
|
||||
|
||||
public class CommandLoader {
|
||||
private static final int BULK_PLAYER_CAPTURE_BATCH_SIZE = 64;
|
||||
private final Set<CommandHandler> guidedSessionCommands = new HashSet<>();
|
||||
private volatile CommandHandler guidedSessionPermissionHandler;
|
||||
private final java.util.concurrent.atomic.AtomicLong guidedPermissionRevision = new java.util.concurrent.atomic.AtomicLong();
|
||||
private final java.util.concurrent.atomic.AtomicBoolean guidedPermissionsManaged = new java.util.concurrent.atomic.AtomicBoolean();
|
||||
private final MaintenanceConfirmation maintenanceConfirmation =
|
||||
new MaintenanceConfirmation();
|
||||
|
||||
@@ -115,6 +121,13 @@ public class CommandLoader {
|
||||
private String playerPerm = "VotingPlugin.Player";
|
||||
|
||||
private VotingPluginMain plugin;
|
||||
private final boolean startupVoteCommandEnabled;
|
||||
private final String startupVoteCommandName;
|
||||
private final boolean startupLoadCommandAliases;
|
||||
private boolean playerCommandConfigured;
|
||||
private PluginCommand playerRootCommand;
|
||||
private boolean voteCommandEnabled;
|
||||
private String voteCommandName = "vote";
|
||||
|
||||
/**
|
||||
* Constructs a new CommandLoader.
|
||||
@@ -123,6 +136,131 @@ public class CommandLoader {
|
||||
*/
|
||||
public CommandLoader(VotingPluginMain plugin) {
|
||||
this.plugin = plugin;
|
||||
var config = plugin.getConfigFile();
|
||||
startupVoteCommandEnabled = config == null || config.isVoteCommandEnabled();
|
||||
startupVoteCommandName = config == null ? "vote" : config.getVoteCommandName();
|
||||
startupLoadCommandAliases = config == null || config.isLoadCommandAliases();
|
||||
voteCommandEnabled = startupVoteCommandEnabled;
|
||||
}
|
||||
|
||||
/** The label actually registered at startup, including its namespace on conflict. */
|
||||
public String getVoteCommandName() {
|
||||
return voteCommandName;
|
||||
}
|
||||
|
||||
public boolean isVoteCommandEnabled() {
|
||||
return voteCommandEnabled;
|
||||
}
|
||||
|
||||
/** Runs a generated player navigation action only while its command is available. */
|
||||
public void performVoteCommand(Player player, String arguments) {
|
||||
if (voteCommandEnabled) {
|
||||
player.performCommand(voteCommandName + (arguments.isEmpty() ? "" : " " + arguments));
|
||||
}
|
||||
}
|
||||
|
||||
/** Configure only once: reloading configuration must not change command ownership. */
|
||||
private void configurePlayerCommand() {
|
||||
if (playerCommandConfigured) return;
|
||||
playerCommandConfigured = true;
|
||||
try {
|
||||
CommandMap commandMap = getCommandMap();
|
||||
PluginCommand root = ownedCommand("vote");
|
||||
playerRootCommand = root;
|
||||
if (!startupVoteCommandEnabled) {
|
||||
for (String name : plugin.getDescription().getCommands().keySet()) {
|
||||
if (name.equalsIgnoreCase("vote") || name.toLowerCase(Locale.ROOT).startsWith("vote")) {
|
||||
PluginCommand command = ownedCommand(name);
|
||||
if (command != null) unregisterCommand(commandMap, command);
|
||||
}
|
||||
}
|
||||
voteCommandEnabled = false;
|
||||
return;
|
||||
}
|
||||
if (root == null) {
|
||||
voteCommandEnabled = false;
|
||||
plugin.getLogger().warning("Unable to locate VotingPlugin's player command; player commands were not enabled.");
|
||||
return;
|
||||
}
|
||||
String name = startupVoteCommandName == null ? "" : startupVoteCommandName.trim().toLowerCase(Locale.ROOT);
|
||||
if (!name.matches("[a-z0-9][a-z0-9_-]{0,63}") || isReservedCommandName(name)) {
|
||||
plugin.getLogger().warning("Invalid or reserved VoteCommandName; using vote. Command settings require a restart.");
|
||||
name = "vote";
|
||||
}
|
||||
String namespace = plugin.getDescription().getName().toLowerCase(Locale.ROOT);
|
||||
String fallback = namespace + ":" + name;
|
||||
org.bukkit.command.Command existingFallback = commandMap.getCommand(fallback);
|
||||
org.bukkit.command.Command existingLabel = commandMap.getCommand(name);
|
||||
boolean ownedLabelCollision = existingLabel instanceof PluginCommand owned
|
||||
&& owned.getPlugin() == plugin && owned != root;
|
||||
if (!name.equals("vote") && ((existingFallback != null && existingFallback != root) || ownedLabelCollision)) {
|
||||
plugin.getLogger().warning("VoteCommandName namespace is already owned; keeping vote. Command settings require a restart.");
|
||||
name = "vote";
|
||||
fallback = namespace + ":vote";
|
||||
}
|
||||
if (!name.equals("vote")) {
|
||||
// Bukkit's register writes the fallback even when the bare label collides.
|
||||
// Check it before unregistering or publishing the renamed command.
|
||||
unregisterCommand(commandMap, root);
|
||||
root.setName(name);
|
||||
root.setLabel(name);
|
||||
root.setAliases(List.of());
|
||||
if (existingLabel != null && existingLabel != root) {
|
||||
// SimpleCommandMap may overwrite another command's bare alias. Register
|
||||
// only our free fallback in that case, preserving the alias owner too.
|
||||
root.setLabel(fallback);
|
||||
if (!root.register(commandMap)) throw new IllegalStateException("Player command registration was rejected");
|
||||
Map<String, org.bukkit.command.Command> registrations = getKnownCommands(commandMap);
|
||||
// Paper forwards put/get to Brigadier; HashMap.putIfAbsent can bypass it.
|
||||
if (registrations.get(fallback) != null) throw new IllegalStateException("Player command namespace became occupied");
|
||||
registrations.put(fallback, root);
|
||||
} else {
|
||||
commandMap.register(namespace, root);
|
||||
}
|
||||
}
|
||||
voteCommandName = commandMap.getCommand(name) == root ? name : fallback;
|
||||
voteCommandEnabled = commandMap.getCommand(voteCommandName) == root;
|
||||
if (voteCommandEnabled) {
|
||||
root.setExecutor(new CommandVote(plugin));
|
||||
root.setTabCompleter(new VoteTabCompleter());
|
||||
if (!voteCommandName.equals(name)) {
|
||||
plugin.getLogger().warning("Player command label is already owned; use /" + voteCommandName + " instead.");
|
||||
}
|
||||
} else {
|
||||
plugin.getLogger().warning("Unable to register VotingPlugin's player command without replacing another owner.");
|
||||
}
|
||||
} catch (ReflectiveOperationException | RuntimeException failure) {
|
||||
throw new IllegalStateException("Unable to configure VotingPlugin's player command safely", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isReservedCommandName(String name) {
|
||||
for (var entry : plugin.getDescription().getCommands().entrySet()) {
|
||||
if (entry.getKey().equalsIgnoreCase("vote")) continue;
|
||||
if (entry.getKey().equalsIgnoreCase(name)) return true;
|
||||
Object aliases = entry.getValue().get("aliases");
|
||||
if (aliases instanceof String alias && alias.equalsIgnoreCase(name)) return true;
|
||||
if (aliases instanceof Iterable<?> values) {
|
||||
for (Object alias : values) if (name.equalsIgnoreCase(String.valueOf(alias))) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private PluginCommand ownedCommand(String name) {
|
||||
PluginCommand command = plugin.getCommand(name);
|
||||
if (command != null && command.getPlugin() == plugin) return command;
|
||||
try {
|
||||
// A plugin may have displaced the primary and fallback labels while a short
|
||||
// alias still points at our command. Disable must remove that object too.
|
||||
for (org.bukkit.command.Command candidate : getKnownCommands(getCommandMap()).values()) {
|
||||
if (candidate instanceof PluginCommand owned && owned.getPlugin() == plugin
|
||||
&& owned.getName().equalsIgnoreCase(name)) return owned;
|
||||
}
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new IllegalStateException("Unable to inspect owned command registrations", failure);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void runForCommandSender(CommandSender sender, Runnable task) {
|
||||
@@ -220,7 +358,7 @@ public class CommandLoader {
|
||||
java.util.List<VotingPluginUser> users = new java.util.ArrayList<>();
|
||||
try {
|
||||
for (String uuidString : plugin.getUserManager().getAllUUIDs()) {
|
||||
users.add(plugin.getVotingPluginUserManager().getVotingPluginUser(UUID.fromString(uuidString)));
|
||||
users.add(plugin.getVotingPluginUserManager().getVotingPluginUser(UUID.fromString(uuidString), true));
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
plugin.debug(failure);
|
||||
@@ -403,26 +541,28 @@ public class CommandLoader {
|
||||
* @return true if player has permission
|
||||
*/
|
||||
public boolean hasPermission(Player player, String cmd) {
|
||||
cmd = cmd.toLowerCase(Locale.ROOT);
|
||||
if (cmd.startsWith("votingplugin:")) {
|
||||
cmd = cmd.substring("votingplugin:".length());
|
||||
}
|
||||
|
||||
if (commands.containsKey(cmd)) {
|
||||
return commands.get(cmd).hasPerm(player);
|
||||
for (var entry : commands.entrySet()) {
|
||||
if (entry.getKey().equalsIgnoreCase(cmd)) return entry.getValue().hasPerm(player);
|
||||
}
|
||||
|
||||
boolean adminCommand = false;
|
||||
String rootLabel = voteCommandName.substring(voteCommandName.indexOf(':') + 1);
|
||||
|
||||
if (cmd.startsWith("vote")) {
|
||||
cmd = cmd.substring("vote".length());
|
||||
} else if (cmd.startsWith("v")) {
|
||||
cmd = cmd.substring(1);
|
||||
} else if (cmd.startsWith("av")) {
|
||||
if (cmd.equals("adminvote") || cmd.startsWith("adminvote ")) {
|
||||
adminCommand = true;
|
||||
cmd = cmd.substring("av".length());
|
||||
} else if (cmd.startsWith("adminvote")) {
|
||||
cmd = cmd.substring("adminvote".length()).trim();
|
||||
} else if (cmd.equals("av") || cmd.startsWith("av ")) {
|
||||
adminCommand = true;
|
||||
cmd = cmd.substring("adminvote".length());
|
||||
cmd = cmd.substring("av".length()).trim();
|
||||
} else if (voteCommandEnabled && (cmd.equals(rootLabel) || cmd.startsWith(rootLabel + " "))) {
|
||||
cmd = cmd.substring(rootLabel.length()).trim();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
// plugin.debug(cmd);
|
||||
if (!adminCommand) {
|
||||
@@ -459,13 +599,11 @@ public class CommandLoader {
|
||||
* @return true if it's a VotingPlugin command
|
||||
*/
|
||||
public boolean isVotingPluginCommand(Player player, String cmd) {
|
||||
if (plugin.getCommand(cmd) != null || cmd.startsWith("votingplugin")) {
|
||||
return true;
|
||||
}
|
||||
for (String str : commands.keySet()) {
|
||||
if (str.equalsIgnoreCase(cmd)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
org.bukkit.command.Command command = getCommandMap().getCommand(cmd);
|
||||
return command instanceof PluginCommand owned && owned.getPlugin() == plugin;
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
plugin.debug(failure);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2969,9 +3107,10 @@ public class CommandLoader {
|
||||
public void loadAliases() {
|
||||
commands = new HashMap<>();
|
||||
aliasCommandNames.clear();
|
||||
configurePlayerCommand();
|
||||
|
||||
// Bukkit registers plugin.yml commands before this loader runs.
|
||||
if (!plugin.getConfigFile().isLoadCommandAliases()) {
|
||||
if (!startupLoadCommandAliases) {
|
||||
unregisterOptionalPluginYMLCommands();
|
||||
} else {
|
||||
unregisterDisabledPluginYMLCommands();
|
||||
@@ -2979,7 +3118,7 @@ public class CommandLoader {
|
||||
|
||||
// If false: still wire permissions, but don't wire alias executors/tab
|
||||
// completers.
|
||||
final boolean enableAliases = plugin.getConfigFile().isLoadCommandAliases();
|
||||
final boolean enableAliases = startupLoadCommandAliases;
|
||||
|
||||
// ---------------------------
|
||||
// /vote aliases
|
||||
@@ -3011,16 +3150,18 @@ public class CommandLoader {
|
||||
}
|
||||
registerAdditionalPermissions(Bukkit.getPluginManager(), cmdHandle);
|
||||
|
||||
if (argLength > 0) {
|
||||
if (argLength > 0 && voteCommandEnabled) {
|
||||
String[] args = cmdHandle.getArgs()[0].split("&");
|
||||
|
||||
for (String arg : args) {
|
||||
String commandName = "vote" + arg;
|
||||
// A renamed root may equal a synthetic alias absent from plugin.yml.
|
||||
if (ownedCommand(commandName) == playerRootCommand && playerRootCommand != null) continue;
|
||||
commands.put(commandName, cmdHandle);
|
||||
aliasCommandNames.add(commandName.toLowerCase(Locale.ROOT));
|
||||
|
||||
try {
|
||||
PluginCommand command = plugin.getCommand(commandName);
|
||||
PluginCommand command = ownedCommand(commandName);
|
||||
if (command == null) {
|
||||
plugin.devDebug("Command " + commandName + " not found in plugin.yml");
|
||||
continue;
|
||||
@@ -3094,11 +3235,13 @@ public class CommandLoader {
|
||||
|
||||
for (String arg : args) {
|
||||
String commandName = "adminvote" + arg;
|
||||
// A renamed root may equal a synthetic alias absent from plugin.yml.
|
||||
if (ownedCommand(commandName) == playerRootCommand && playerRootCommand != null) continue;
|
||||
commands.put(commandName, cmdHandle);
|
||||
aliasCommandNames.add(commandName.toLowerCase(Locale.ROOT));
|
||||
|
||||
try {
|
||||
PluginCommand command = plugin.getCommand(commandName);
|
||||
PluginCommand command = ownedCommand(commandName);
|
||||
if (command == null) {
|
||||
plugin.devDebug("Command " + commandName + " not found in plugin.yml");
|
||||
continue;
|
||||
@@ -3135,6 +3278,7 @@ public class CommandLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
refreshGuidedSessionPermissionIndex();
|
||||
}
|
||||
|
||||
static void registerAdditionalPermissions(PluginManager manager, CommandHandler handler) {
|
||||
@@ -3164,8 +3308,8 @@ public class CommandLoader {
|
||||
|| commandName.equalsIgnoreCase("av")) {
|
||||
continue;
|
||||
}
|
||||
PluginCommand command = plugin.getCommand(commandName);
|
||||
if (command != null) {
|
||||
PluginCommand command = ownedCommand(commandName);
|
||||
if (command != null && command != playerRootCommand) {
|
||||
unregisterCommand(commandMap, command);
|
||||
}
|
||||
}
|
||||
@@ -3179,8 +3323,8 @@ public class CommandLoader {
|
||||
try {
|
||||
CommandMap commandMap = getCommandMap();
|
||||
for (String commandName : disabledAliasCommandNames) {
|
||||
PluginCommand command = plugin.getCommand(commandName);
|
||||
if (command != null) {
|
||||
PluginCommand command = ownedCommand(commandName);
|
||||
if (command != null && command != playerRootCommand) {
|
||||
unregisterCommand(commandMap, command);
|
||||
}
|
||||
}
|
||||
@@ -3192,22 +3336,7 @@ public class CommandLoader {
|
||||
/** Removes the command and all labels that still point to it from Bukkit's map. */
|
||||
private void unregisterCommand(CommandMap commandMap, PluginCommand command) throws ReflectiveOperationException {
|
||||
command.unregister(commandMap);
|
||||
java.lang.reflect.Field field = null;
|
||||
Class<?> commandMapClass = commandMap.getClass();
|
||||
while (commandMapClass != null && field == null) {
|
||||
try {
|
||||
field = commandMapClass.getDeclaredField("knownCommands");
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
commandMapClass = commandMapClass.getSuperclass();
|
||||
}
|
||||
}
|
||||
if (field == null) {
|
||||
throw new NoSuchFieldException("knownCommands");
|
||||
}
|
||||
field.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, org.bukkit.command.Command> knownCommands =
|
||||
(Map<String, org.bukkit.command.Command>) field.get(commandMap);
|
||||
Map<String, org.bukkit.command.Command> knownCommands = getKnownCommands(commandMap);
|
||||
Set<String> removedLabels = new HashSet<>();
|
||||
for (Map.Entry<String, org.bukkit.command.Command> entry : knownCommands.entrySet()) {
|
||||
if (entry.getValue() == command) {
|
||||
@@ -3236,6 +3365,27 @@ public class CommandLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads the same Bukkit-owned map used by exact-object unregister. */
|
||||
private Map<String, org.bukkit.command.Command> getKnownCommands(CommandMap commandMap) throws ReflectiveOperationException {
|
||||
java.lang.reflect.Field field = null;
|
||||
Class<?> commandMapClass = commandMap.getClass();
|
||||
while (commandMapClass != null && field == null) {
|
||||
try {
|
||||
field = commandMapClass.getDeclaredField("knownCommands");
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
commandMapClass = commandMapClass.getSuperclass();
|
||||
}
|
||||
}
|
||||
if (field == null) {
|
||||
throw new NoSuchFieldException("knownCommands");
|
||||
}
|
||||
field.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, org.bukkit.command.Command> knownCommands =
|
||||
(Map<String, org.bukkit.command.Command>) field.get(commandMap);
|
||||
return knownCommands;
|
||||
}
|
||||
|
||||
/** Gets Bukkit's command map without depending on a CraftBukkit package name. */
|
||||
private CommandMap getCommandMap() throws ReflectiveOperationException {
|
||||
java.lang.reflect.Method method = plugin.getServer().getClass().getMethod("getCommandMap");
|
||||
@@ -3484,7 +3634,83 @@ public class CommandLoader {
|
||||
/**
|
||||
* Load vote command.
|
||||
*/
|
||||
/** Refresh only guide-owned handlers; preserve production/custom registrations and command-map aliases. */
|
||||
public void refreshGuidedSessionCommands() {
|
||||
boolean previouslyRegistered = !guidedSessionCommands.isEmpty();
|
||||
long permissionRevision = guidedPermissionRevision.incrementAndGet();
|
||||
ArrayList<CommandHandler> handlers = new ArrayList<>(plugin.getVoteCommand());
|
||||
handlers.removeAll(guidedSessionCommands);
|
||||
guidedSessionCommands.clear();
|
||||
if (plugin.getConfigFile().getData().getBoolean("GuidedVotingSession.Enabled", false)) {
|
||||
for (String action : java.util.List.of("", "next", "previous", "skip", "check", "finish", "restart")) {
|
||||
String[] path = action.isEmpty() ? new String[] { "Session" } : new String[] { "Session", action };
|
||||
var sessionCommand = plugin.getGuidedVotingSessions().commandHandler(action);
|
||||
CommandHandler handler = new CommandHandler(plugin, path,
|
||||
"VotingPlugin.Commands.Vote.Session|" + playerPerm, "Guided voting session", false) {
|
||||
@Override public void execute(CommandSender sender, String[] args) {
|
||||
sessionCommand.accept((Player) sender);
|
||||
}
|
||||
};
|
||||
if (plugin.getConfigFile().getDisabledCommands().stream().noneMatch(handler.getPerm()::contains)) {
|
||||
if (plugin.getConfigFile().getDisabledDefaultPermissions().stream()
|
||||
.anyMatch(disabled -> handler.getPerm().contains(disabled + "|"))) {
|
||||
handler.setPerm(handler.getPerm().replace("|" + playerPerm, ""));
|
||||
}
|
||||
guidedSessionCommands.add(handler);
|
||||
handlers.add(guidedSessionCommands.size() - 1, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.setVoteCommand(handlers);
|
||||
refreshGuidedSessionPermissionIndex();
|
||||
if (previouslyRegistered || !guidedSessionCommands.isEmpty()) guidedPermissionsManaged.set(true);
|
||||
if (guidedPermissionsManaged.get()) {
|
||||
String inheritedPermission = "VotingPlugin.Commands.Vote.Session|" + playerPerm;
|
||||
boolean inherit = plugin.getConfigFile().getDisabledDefaultPermissions().stream()
|
||||
.noneMatch(disabled -> inheritedPermission.contains(disabled + "|"))
|
||||
&& handlers.stream().anyMatch(handler -> handler.getPerm().contains(inheritedPermission));
|
||||
plugin.getBukkitScheduler().runTask(plugin, () -> {
|
||||
if (guidedPermissionRevision.get() != permissionRevision) return;
|
||||
Permission parent = Bukkit.getPluginManager().getPermission(playerPerm);
|
||||
if (parent == null) return;
|
||||
if (inherit) parent.getChildren().put("VotingPlugin.Commands.Vote.Session", true);
|
||||
else parent.getChildren().remove("VotingPlugin.Commands.Vote.Session");
|
||||
parent.recalculatePermissibles();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Use the registered built-in command policy for guide-owned callbacks and refreshes. */
|
||||
public boolean canUseGuidedSession(Player player) {
|
||||
CommandHandler handler = guidedSessionPermissionHandler;
|
||||
return handler != null && handler.hasPerm(player);
|
||||
}
|
||||
|
||||
/** Keep the session lookup consistent with routing without registering or replacing Bukkit aliases. */
|
||||
private void refreshGuidedSessionPermissionIndex() {
|
||||
CommandHandler selected = null;
|
||||
for (CommandHandler handler : plugin.getVoteCommand()) {
|
||||
if (!handler.isCommand("Session")) continue;
|
||||
if (guidedSessionCommands.contains(handler)) {
|
||||
if (handler.getArgs().length == 1) {
|
||||
selected = handler;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Preserve the existing alias index's last-handler policy when the guide is disabled.
|
||||
selected = handler;
|
||||
}
|
||||
}
|
||||
guidedSessionPermissionHandler = selected != null && guidedSessionCommands.contains(selected) ? selected : null;
|
||||
if (commands == null) return;
|
||||
final CommandHandler indexed = selected;
|
||||
commands.entrySet().removeIf(entry -> entry.getKey().equalsIgnoreCase("voteSession"));
|
||||
if (indexed != null) commands.put("voteSession", indexed);
|
||||
}
|
||||
|
||||
private void loadVoteCommand() {
|
||||
guidedSessionPermissionHandler = null;
|
||||
guidedSessionCommands.clear();
|
||||
plugin.setVoteCommand(new ArrayList<>());
|
||||
plugin.getVoteCommand().add(new CommandHandler(plugin, new String[] { "Help&?" },
|
||||
"VotingPlugin.Commands.Vote.Help|" + playerPerm, "View help page") {
|
||||
@@ -3551,6 +3777,8 @@ public class CommandLoader {
|
||||
}
|
||||
});
|
||||
|
||||
refreshGuidedSessionCommands();
|
||||
|
||||
plugin.getVoteCommand().add(new CommandHandler(plugin, new String[] { "URL", "(SiteName)" },
|
||||
"VotingPlugin.Commands.Vote.URL.VoteSite", "Open VoteURL GUI for VoteSite", false) {
|
||||
|
||||
@@ -4069,6 +4297,9 @@ public class CommandLoader {
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve administrator-defined matches ahead of this new fallback.
|
||||
com.bencodez.votingplugin.streaks.VoteStreaksCommand.register(plugin);
|
||||
|
||||
ArrayList<CommandHandler> avCommands = com.bencodez.advancedcore.command.CommandLoader.getInstance()
|
||||
.getBasicCommands("VotingPlugin");
|
||||
for (CommandHandler cmd : avCommands) {
|
||||
@@ -4135,7 +4366,7 @@ public class CommandLoader {
|
||||
} else if (MessageAPI.startsWithIgnoreCase(slot, "today")) {
|
||||
new VoteToday(plugin, player, user, 1).open();
|
||||
} else if (MessageAPI.startsWithIgnoreCase(slot, "help")) {
|
||||
player.performCommand("vote help");
|
||||
performVoteCommand(player, "help");
|
||||
} else if (MessageAPI.startsWithIgnoreCase(slot, "shop")) {
|
||||
new VoteShop(plugin, player, user).open();
|
||||
} else if (MessageAPI.startsWithIgnoreCase(slot, "lastmonth")) {
|
||||
@@ -4144,6 +4375,8 @@ public class CommandLoader {
|
||||
new VoteBest(plugin, player, user).open();
|
||||
} else if (MessageAPI.startsWithIgnoreCase(slot, "streak")) {
|
||||
new VoteStreak(plugin, player, user).open();
|
||||
} else if (MessageAPI.startsWithIgnoreCase(slot, "streaks")) {
|
||||
plugin.getVoteStreaksMenu().open(player);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ public class CommandAliases implements CommandExecutor {
|
||||
if (adminCommand) {
|
||||
sender.sendMessage(MessageAPI.colorize(plugin.getConfigFile().getFormatInvalidCommandAdminVote()));
|
||||
} else {
|
||||
sender.sendMessage(MessageAPI.colorize(plugin.getConfigFile().getFormatInvalidCommandVote()));
|
||||
sender.sendMessage(MessageAPI.colorize(plugin.formatVoteCommandMessage(plugin.getConfigFile().getFormatInvalidCommandVote())));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ public class CommandVote implements CommandExecutor {
|
||||
}
|
||||
|
||||
// invalid command
|
||||
sender.sendMessage(MessageAPI.colorize(plugin.getConfigFile().getFormatInvalidCommandVote()));
|
||||
sender.sendMessage(MessageAPI.colorize(plugin.formatVoteCommandMessage(plugin.getConfigFile().getFormatInvalidCommandVote())));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+8
-8
@@ -77,16 +77,16 @@ public class AdminVotePerms extends GUIHandler {
|
||||
for (CommandHandler handle : plugin.getVoteCommand()) {
|
||||
if (sender instanceof Player) {
|
||||
if (handle.hasPerm(sender)) {
|
||||
msg.add("&6" + handle.getHelpLineCommand("/vote") + " : "
|
||||
msg.add("&6" + handle.getHelpLineCommand(plugin.getVoteCommandPrefix()) + " : "
|
||||
+ handle.getPerm().split(Pattern.quote("|"))[0] + " : &atrue");
|
||||
} else {
|
||||
msg.add("&6" + handle.getHelpLineCommand("/vote") + " : "
|
||||
msg.add("&6" + handle.getHelpLineCommand(plugin.getVoteCommandPrefix()) + " : "
|
||||
+ handle.getPerm().split(Pattern.quote("|"))[0] + " : &cfalse");
|
||||
}
|
||||
} else {
|
||||
msg.add(handle.getHelpLineCommand("/vote") + " : " + handle.getPerm().split(Pattern.quote("|"))[0]);
|
||||
msg.add(handle.getHelpLineCommand(plugin.getVoteCommandPrefix()) + " : " + handle.getPerm().split(Pattern.quote("|"))[0]);
|
||||
}
|
||||
addAdditionalPermissions(msg, sender, handle, "/vote");
|
||||
addAdditionalPermissions(msg, sender, handle, plugin.getVoteCommandPrefix());
|
||||
|
||||
}
|
||||
|
||||
@@ -164,13 +164,13 @@ public class AdminVotePerms extends GUIHandler {
|
||||
|
||||
for (CommandHandler handle : plugin.getVoteCommand()) {
|
||||
if (handle.hasPerm(p)) {
|
||||
msg.add("&6" + handle.getHelpLineCommand("/vote") + " : "
|
||||
msg.add("&6" + handle.getHelpLineCommand(plugin.getVoteCommandPrefix()) + " : "
|
||||
+ handle.getPerm().split(Pattern.quote("|"))[0] + " : &atrue");
|
||||
} else {
|
||||
msg.add("&6" + handle.getHelpLineCommand("/vote") + " : "
|
||||
msg.add("&6" + handle.getHelpLineCommand(plugin.getVoteCommandPrefix()) + " : "
|
||||
+ handle.getPerm().split(Pattern.quote("|"))[0] + " : &cfalse");
|
||||
}
|
||||
addAdditionalPermissions(msg, p, handle, "/vote");
|
||||
addAdditionalPermissions(msg, p, handle, plugin.getVoteCommandPrefix());
|
||||
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ public class AdminVotePerms extends GUIHandler {
|
||||
msg.add(" ");
|
||||
|
||||
for (CommandHandler handle : plugin.getVoteCommand()) {
|
||||
msg.add(handle.getHelpLineCommand("/vote"));
|
||||
msg.add(handle.getHelpLineCommand(plugin.getVoteCommandPrefix()));
|
||||
msg.add(" " + handle.getPerm());
|
||||
msg.add(" " + handle.getHelpMessage());
|
||||
for (String permission : additionalPermissions(handle)) {
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ public class AdminVoteTopPoints extends GUIHandler {
|
||||
LinkedHashMap<TopVoterPlayer, Integer> raw = new LinkedHashMap<>();
|
||||
for (String uuidString : plugin.getVotingPluginUserManager().getAllUUIDs()) {
|
||||
UUID uuid = UUID.fromString(uuidString);
|
||||
VotingPluginUser vpUser = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid);
|
||||
VotingPluginUser vpUser = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid, true);
|
||||
vpUser.userDataFetechMode(UserDataFetchMode.NO_CACHE);
|
||||
int points = vpUser.getPoints();
|
||||
if (points <= 0) continue;
|
||||
|
||||
+5
-3
@@ -55,6 +55,7 @@ public class VoteHelp extends GUIHandler {
|
||||
*/
|
||||
public ArrayList<TextComponent> helpText(CommandSender sender) {
|
||||
ArrayList<TextComponent> msg = new ArrayList<>();
|
||||
if (!plugin.isVoteCommandEnabled()) return msg;
|
||||
HashMap<String, TextComponent> unsorted = new HashMap<>();
|
||||
|
||||
boolean requirePerms = plugin.getConfigFile().isFormatCommandsVoteHelpRequirePermission();
|
||||
@@ -70,7 +71,7 @@ public class VoteHelp extends GUIHandler {
|
||||
}
|
||||
for (CommandHandler cmdHandle : plugin.getVoteCommand()) {
|
||||
if (!requirePerms || cmdHandle.hasPerm(sender)) {
|
||||
unsorted.put(cmdHandle.getHelpLineCommand("/vote"), cmdHandle.getHelpLine("/vote",
|
||||
unsorted.put(cmdHandle.getHelpLineCommand(plugin.getVoteCommandPrefix()), cmdHandle.getHelpLine(plugin.getVoteCommandPrefix(),
|
||||
plugin.getConfigFile().getFormatCommandsVoteHelpLine(), hoverColor));
|
||||
}
|
||||
}
|
||||
@@ -92,14 +93,15 @@ public class VoteHelp extends GUIHandler {
|
||||
*/
|
||||
public ArrayList<TextComponent> helpTextLegacy(CommandSender sender) {
|
||||
ArrayList<TextComponent> msg = new ArrayList<>();
|
||||
if (!plugin.isVoteCommandEnabled()) return msg;
|
||||
HashMap<String, TextComponent> unsorted = new HashMap<>();
|
||||
|
||||
boolean requirePerms = plugin.getConfigFile().isFormatCommandsVoteHelpRequirePermission();
|
||||
|
||||
for (CommandHandler cmdHandle : plugin.getVoteCommand()) {
|
||||
if (!requirePerms || cmdHandle.hasPerm(sender)) {
|
||||
unsorted.put(cmdHandle.getHelpLineCommand("/vote"),
|
||||
cmdHandle.getHelpLine("/vote", plugin.getConfigFile().getFormatCommandsVoteHelpLine()));
|
||||
unsorted.put(cmdHandle.getHelpLineCommand(plugin.getVoteCommandPrefix()),
|
||||
cmdHandle.getHelpLine(plugin.getVoteCommandPrefix(), plugin.getConfigFile().getFormatCommandsVoteHelpLine()));
|
||||
}
|
||||
}
|
||||
ArrayList<String> unsortedList = new ArrayList<>();
|
||||
|
||||
+2
-2
@@ -60,7 +60,7 @@ public class VoteToday extends GUIHandler {
|
||||
Player clicked = player.getServer().getPlayer(payload.owner());
|
||||
if (clicked != null) {
|
||||
VotingPluginUser clickedUser = plugin.getVotingPluginUserManager()
|
||||
.getVotingPluginUser(uuid);
|
||||
.getVotingPluginUser(uuid, true);
|
||||
new VoteGUI(plugin, clicked, clickedUser).open(
|
||||
GUIMethod.valueOf(plugin.getGui().getGuiMethodGUI().toUpperCase()));
|
||||
}
|
||||
@@ -161,7 +161,7 @@ public class VoteToday extends GUIHandler {
|
||||
inv.addButton(inv.getNextSlot(), new BInventoryButton(item) {
|
||||
@Override
|
||||
public void onClick(ClickEvent clickEvent) {
|
||||
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid);
|
||||
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid, true);
|
||||
new VoteGUI(plugin, player, user)
|
||||
.open(GUIMethod.valueOf(plugin.getGui().getGuiMethodGUI().toUpperCase()));
|
||||
}
|
||||
|
||||
+1
-1
@@ -177,7 +177,7 @@ public class VoteURL extends GUIHandler {
|
||||
}
|
||||
|
||||
if (plugin.getGui().isChestVoteURLBackButton()) {
|
||||
dialog.button("&eBack", "&7Return to previous menu", payload -> player.performCommand("vote gui"));
|
||||
dialog.button("&eBack", "&7Return to previous menu", payload -> plugin.getCommandLoader().performVoteCommand(player, "gui"));
|
||||
}
|
||||
|
||||
dialog.open();
|
||||
|
||||
@@ -522,6 +522,15 @@ public class Config extends YMLFile {
|
||||
@Getter
|
||||
private boolean limitMonthlyVotes = false;
|
||||
|
||||
/** Restart-only player command registration. Missing keys preserve /vote. */
|
||||
@ConfigDataBoolean(path = "VoteCommandEnabled", defaultValue = true)
|
||||
@Getter
|
||||
private boolean voteCommandEnabled = true;
|
||||
|
||||
@ConfigDataString(path = "VoteCommandName", defaultValue = "vote")
|
||||
@Getter
|
||||
private String voteCommandName = "vote";
|
||||
|
||||
@ConfigDataBoolean(path = "LoadCommandAliases")
|
||||
@Getter
|
||||
private boolean loadCommandAliases = true;
|
||||
|
||||
@@ -26,6 +26,10 @@ import lombok.Getter;
|
||||
*/
|
||||
public class GUI extends YMLFile {
|
||||
|
||||
@ConfigDataString(path = "CHEST.VoteStreaks.Name")
|
||||
@Getter
|
||||
private String chestVoteStreaksName = "&6Vote streaks";
|
||||
|
||||
@ConfigDataString(path = "BOOK.VoteURLBookGUI.AlreadyVotedColor")
|
||||
@Getter
|
||||
private String bookVoteURLBookGUIAlreadyVotedColor = "RED";
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package com.bencodez.votingplugin.core.maintenance;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Cumulative active-runtime clock for the owner-facing auto-create setup reminder.
|
||||
* This class performs no persistence or notification. One owned worker must
|
||||
* serialize the entire propose, persist, commit, then notify sequence, including
|
||||
* reload and shutdown checkpoints. Method synchronization alone does not serialize
|
||||
* the external persistence step. Only notify after a successful commit.
|
||||
*
|
||||
* <p>Reconstruct this state from its durable snapshot and a fresh monotonic clock
|
||||
* reading on startup; never persist the process-local clock anchor. Samples must
|
||||
* be less than {@link Long#MAX_VALUE} nanoseconds apart (the normal nanoTime elapsed
|
||||
* time contract). Negative clock readings and signed clock wrap are supported.
|
||||
*/
|
||||
public final class AutoCreateVoteSitesReminderState {
|
||||
|
||||
public static final long NEVER_REMINDED = -1;
|
||||
/** The first reminder is emitted on the first active checkpoint. */
|
||||
public static final long DEFAULT_THRESHOLD_MILLIS = 0;
|
||||
/** Subsequent reminders are bounded to one every 48 active hours. */
|
||||
public static final long DEFAULT_REPEAT_MILLIS = TimeUnit.HOURS.toMillis(48);
|
||||
public static final long MAX_INTERVAL_MILLIS = Long.MAX_VALUE / 1_000_000;
|
||||
private static final long NANOS_PER_MILLI = 1_000_000;
|
||||
|
||||
/** Immutable durable data; a reminder marker must refer to accrued runtime. */
|
||||
public record Snapshot(long totalRuntimeMillis, long lastReminderRuntimeMillis) {
|
||||
public Snapshot {
|
||||
if (totalRuntimeMillis < 0 || lastReminderRuntimeMillis < NEVER_REMINDED
|
||||
|| lastReminderRuntimeMillis > totalRuntimeMillis) {
|
||||
throw new IllegalArgumentException("Invalid reminder runtime snapshot");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A tentative checkpoint, valid only until this state commits any candidate.
|
||||
* Persist {@link #snapshot()} before committing; discard on a failed write and
|
||||
* propose again. Creating or discarding candidates changes no committed state.
|
||||
*/
|
||||
public static final class Candidate {
|
||||
private final Object epoch;
|
||||
private final Snapshot snapshot;
|
||||
private final long checkpointNanos;
|
||||
private final boolean reminderDue;
|
||||
|
||||
private Candidate(Object epoch, Snapshot snapshot, long checkpointNanos, boolean reminderDue) {
|
||||
this.epoch = epoch;
|
||||
this.snapshot = snapshot;
|
||||
this.checkpointNanos = checkpointNanos;
|
||||
this.reminderDue = reminderDue;
|
||||
}
|
||||
|
||||
public Snapshot snapshot() {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public boolean reminderDue() {
|
||||
return reminderDue;
|
||||
}
|
||||
}
|
||||
|
||||
private Snapshot snapshot;
|
||||
private long checkpointNanos;
|
||||
private Object epoch = new Object();
|
||||
|
||||
public AutoCreateVoteSitesReminderState(Snapshot snapshot, long nowNanos) {
|
||||
this.snapshot = Objects.requireNonNull(snapshot, "snapshot");
|
||||
this.checkpointNanos = nowNanos;
|
||||
}
|
||||
|
||||
public synchronized Snapshot snapshot() {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accrue runtime regardless of either flag. Both flags are required to propose a
|
||||
* reminder, at the inclusive threshold or repeat boundary. Settings are sampled
|
||||
* for each proposal; changing them does not reset accumulated runtime or markers.
|
||||
* Backward samples accrue nothing and leave the clock anchor unchanged.
|
||||
*/
|
||||
public synchronized Candidate propose(long nowNanos, boolean reminderEnabled, boolean autoCreateEnabled,
|
||||
long thresholdMillis, long repeatMillis) {
|
||||
validateInterval(thresholdMillis);
|
||||
validateInterval(repeatMillis);
|
||||
if (repeatMillis == 0) {
|
||||
throw new IllegalArgumentException("Reminder repeat interval must be positive");
|
||||
}
|
||||
long elapsedNanos = nowNanos - checkpointNanos;
|
||||
long elapsedMillis = elapsedNanos > 0 ? elapsedNanos / NANOS_PER_MILLI : 0;
|
||||
long total = saturatingAdd(snapshot.totalRuntimeMillis(), elapsedMillis);
|
||||
long lastReminder = snapshot.lastReminderRuntimeMillis();
|
||||
boolean due = reminderEnabled && autoCreateEnabled && total >= thresholdMillis
|
||||
&& (lastReminder == NEVER_REMINDED || total - lastReminder >= repeatMillis);
|
||||
Snapshot next = new Snapshot(total, due ? total : lastReminder);
|
||||
// Consume only whole milliseconds so frequent checkpoints do not lose time.
|
||||
long nextCheckpoint = checkpointNanos + elapsedMillis * NANOS_PER_MILLI;
|
||||
return new Candidate(epoch, next, nextCheckpoint, due);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a checkpoint only after its durable write succeeded. A successful
|
||||
* commit invalidates every other candidate from that epoch, including this one.
|
||||
* Foreign, stale, or duplicate candidates are rejected without changing state.
|
||||
*/
|
||||
public synchronized void commit(Candidate candidate) {
|
||||
Objects.requireNonNull(candidate, "candidate");
|
||||
if (candidate.epoch != epoch) {
|
||||
throw new IllegalArgumentException("Candidate is not current for this reminder state");
|
||||
}
|
||||
snapshot = candidate.snapshot;
|
||||
checkpointNanos = candidate.checkpointNanos;
|
||||
epoch = new Object();
|
||||
}
|
||||
|
||||
private static void validateInterval(long millis) {
|
||||
if (millis < 0 || millis > MAX_INTERVAL_MILLIS) {
|
||||
throw new IllegalArgumentException("Reminder interval is outside the supported range");
|
||||
}
|
||||
}
|
||||
|
||||
private static long saturatingAdd(long total, long elapsed) {
|
||||
return elapsed > Long.MAX_VALUE - total ? Long.MAX_VALUE : total + elapsed;
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
package com.bencodez.votingplugin.core.session;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Ephemeral, observation-only progress. Never invokes vote or reward processing. */
|
||||
public final class GuidedVoteSession {
|
||||
public enum Status { AWAITING, RECEIVED, UNAVAILABLE, SKIPPED }
|
||||
public record Site(String key, String name, String url, boolean eligible, long lastVote) { }
|
||||
public record Entry(Site site, Status status) { }
|
||||
public record View(List<Entry> entries, int index, boolean finished) {
|
||||
public Entry current() { return entries.isEmpty() ? null : entries.get(index); }
|
||||
public long received() { return entries.stream().filter(e -> e.status() == Status.RECEIVED).count(); }
|
||||
public boolean complete() { return !entries.isEmpty() && entries.stream().allMatch(e -> e.status() == Status.RECEIVED); }
|
||||
}
|
||||
private final long started;
|
||||
private final long observationOrder;
|
||||
private final Map<String, Site> candidates = new LinkedHashMap<>();
|
||||
private boolean candidatesBound;
|
||||
private final Map<String, Site> sites = new LinkedHashMap<>();
|
||||
private final Map<String, Long> received = new LinkedHashMap<>();
|
||||
private final java.util.Set<String> skipped = new java.util.HashSet<>();
|
||||
private final Map<UUID, String> occurrences = new LinkedHashMap<>();
|
||||
private final Map<UUID, ProxyVoteSequence.Fence> proxyFences = new LinkedHashMap<>();
|
||||
private final Map<UUID, String> pendingProxy = new LinkedHashMap<>();
|
||||
private boolean initialized;
|
||||
private boolean finished;
|
||||
private int index;
|
||||
|
||||
public GuidedVoteSession(long started) { this(started, VoteObservationSequence.next()); }
|
||||
public GuidedVoteSession(long started, long observationOrder) { this.started = started; this.observationOrder = observationOrder; }
|
||||
public long started() { return started; }
|
||||
|
||||
/** Bind visible identities on the player context before asynchronous eligibility sampling. */
|
||||
public synchronized void bindCandidates(List<Site> available) {
|
||||
if (candidatesBound) return;
|
||||
for (Site site : available) if (candidates.size() < 100) candidates.put(site.key(), site);
|
||||
candidatesBound = true;
|
||||
}
|
||||
public synchronized void refresh(List<Site> available) {
|
||||
bindCandidates(available);
|
||||
if (!initialized) {
|
||||
// A cooldown timestamp alone does not establish original ingress order.
|
||||
for (Site site : available) if (candidates.containsKey(site.key()) && (site.eligible() || received.containsKey(site.key())) && sites.size() < 100) sites.put(site.key(), site);
|
||||
// A temporarily hidden/removed site can be absent from the worker sample.
|
||||
// Keep its confirmed identity so visibility restoration can recover progress.
|
||||
for (Site candidate : candidates.values()) if (received.containsKey(candidate.key()))
|
||||
sites.putIfAbsent(candidate.key(), candidate);
|
||||
initialized = true;
|
||||
}
|
||||
Map<String, Site> current = new LinkedHashMap<>();
|
||||
for (Site site : available) current.put(site.key(), site);
|
||||
sites.replaceAll((key, old) -> {
|
||||
Site fresh = current.get(key);
|
||||
return fresh == null ? new Site(key, old.name(), old.url(), false, old.lastVote()) : fresh;
|
||||
});
|
||||
}
|
||||
|
||||
public synchronized boolean candidate(String key) { return candidates.containsKey(key); }
|
||||
/** The accepted pipeline supplies stable IDs and its authoritative occurrence time. */
|
||||
public synchronized void accepted(String site, UUID occurrence, long time, boolean real, boolean cancelled) {
|
||||
if (real && !cancelled && occurrence != null && time > started && candidates.containsKey(site)
|
||||
&& !received.containsKey(site) && !occurrences.containsKey(occurrence)) {
|
||||
received.put(site, time);
|
||||
occurrences.put(occurrence, site);
|
||||
// A credited vote may make the first asynchronous eligibility sample unavailable.
|
||||
// Its identity was already bound, so a later notification can restore that entry.
|
||||
if (initialized) sites.putIfAbsent(site, candidates.get(site));
|
||||
}
|
||||
}
|
||||
/** Fresh local ingress uses backend-local order; proxy occurrences require their source fence. */
|
||||
public synchronized void acceptedObserved(String site, UUID occurrence, long order) {
|
||||
if (order > observationOrder && occurrence != null && candidates.containsKey(site)
|
||||
&& !received.containsKey(site) && !occurrences.containsKey(occurrence)) {
|
||||
received.put(site, order);
|
||||
occurrences.put(occurrence, site);
|
||||
if (initialized) sites.putIfAbsent(site, candidates.get(site));
|
||||
}
|
||||
}
|
||||
/** A negotiated source fence is fixed for the entire session, not advanced by status checks. */
|
||||
public synchronized void bindProxyFence(ProxyVoteSequence.Fence fence) {
|
||||
if (!proxyFences.containsKey(fence.incarnation()) && proxyFences.size() < 32) {
|
||||
proxyFences.put(fence.incarnation(), fence);
|
||||
var pending = new ArrayList<>(pendingProxy.entrySet());
|
||||
for (var receipt : pending) acceptedProxy(receipt.getValue(), receipt.getKey());
|
||||
}
|
||||
}
|
||||
public synchronized boolean hasProxyFence() { return !proxyFences.isEmpty(); }
|
||||
/** The normal credited pipeline supplies the original canonical identity, including delayed votes. */
|
||||
public synchronized void acceptedProxy(String site, UUID occurrence) {
|
||||
if (occurrence == null || occurrence.version() != 8 || occurrence.variant() != 2
|
||||
|| !candidates.containsKey(site) || received.containsKey(site) || occurrences.containsKey(occurrence)) return;
|
||||
UUID source = new UUID(occurrence.getMostSignificantBits(), 0x8000000000000000L);
|
||||
ProxyVoteSequence.Fence fence = proxyFences.get(source);
|
||||
if (fence == null) {
|
||||
// Bounded provisional receipts cover a new occurrence overtaking its fence reply.
|
||||
if (pendingProxy.size() < 400) pendingProxy.putIfAbsent(occurrence, site);
|
||||
return;
|
||||
}
|
||||
pendingProxy.remove(occurrence);
|
||||
if (!fence.newer(occurrence)) return;
|
||||
received.put(site, occurrence.getLeastSignificantBits() & 0x3fffffffffffffffL);
|
||||
occurrences.put(occurrence, site);
|
||||
if (initialized) sites.putIfAbsent(site, candidates.get(site));
|
||||
}
|
||||
/**
|
||||
* Select the next eligible site for automatic guidance after the current site
|
||||
* can no longer be the active target. Explicit navigation remains owned by
|
||||
* {@link #view(String)}; this method only advances from a received,
|
||||
* unavailable, or skipped current entry.
|
||||
*
|
||||
* @return whether the active index changed
|
||||
*/
|
||||
public synchronized boolean selectNextAwaiting() {
|
||||
if (finished || sites.isEmpty()) return false;
|
||||
int start = Math.floorMod(index, sites.size());
|
||||
String current = new ArrayList<>(sites.keySet()).get(start);
|
||||
Status currentStatus = status(sites.get(current));
|
||||
if (currentStatus == Status.AWAITING) return false;
|
||||
List<Site> ordered = new ArrayList<>(sites.values());
|
||||
for (int offset = 1; offset <= ordered.size(); offset++) {
|
||||
int candidateIndex = (start + offset) % ordered.size();
|
||||
Site candidate = ordered.get(candidateIndex);
|
||||
if (status(candidate) == Status.AWAITING && candidate.eligible() && !skipped.contains(candidate.key())) {
|
||||
if (candidateIndex == index) return false;
|
||||
index = candidateIndex;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Status status(Site site) {
|
||||
if (received.containsKey(site.key())) return Status.RECEIVED;
|
||||
if (!site.eligible()) return Status.UNAVAILABLE;
|
||||
return skipped.contains(site.key()) ? Status.SKIPPED : Status.AWAITING;
|
||||
}
|
||||
public synchronized View view(String action) {
|
||||
if (!sites.isEmpty()) {
|
||||
String current = new ArrayList<>(sites.keySet()).get(index);
|
||||
if (action.equals("skip")) { skipped.add(current); index = (index + 1) % sites.size(); }
|
||||
if (action.equals("next")) index = (index + 1) % sites.size();
|
||||
if (action.equals("previous")) index = Math.floorMod(index - 1, sites.size());
|
||||
}
|
||||
if (action.equals("finish")) finished = true;
|
||||
List<Entry> entries = new ArrayList<>();
|
||||
for (Site site : sites.values()) {
|
||||
entries.add(new Entry(site, status(site)));
|
||||
}
|
||||
return new View(List.copyOf(entries), index, finished);
|
||||
}
|
||||
public static String httpUrl(String input) {
|
||||
if (input == null || input.length() > 2048) return null;
|
||||
try {
|
||||
String destination = input.trim();
|
||||
if (destination.startsWith("[")) {
|
||||
var wrapper = java.util.regex.Pattern.compile("(?i)\\[Text=\"[^\"\\r\\n]*\",\\s*url=\"([^\"\\r\\n]*)\"\\]").matcher(destination);
|
||||
if (!wrapper.matches()) return null;
|
||||
destination = wrapper.group(1);
|
||||
}
|
||||
URI uri = URI.create(destination);
|
||||
return ("http".equalsIgnoreCase(uri.getScheme()) || "https".equalsIgnoreCase(uri.getScheme()))
|
||||
&& uri.getHost() != null && uri.getUserInfo() == null ? uri.toASCIIString() : null;
|
||||
} catch (IllegalArgumentException invalid) { return null; }
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.bencodez.votingplugin.core.session;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Original proxy ingress order, carried by the existing opaque canonical vote UUID.
|
||||
* UUIDv8: random 60-bit source incarnation and a monotonically increasing 62-bit sequence.
|
||||
* The process/classloader owns one source across runtime replacement; a restart creates
|
||||
* another. Never assign a new ID when forwarding, retrying or replaying an occurrence. */
|
||||
public final class ProxyVoteSequence {
|
||||
private static final long MAX_SEQUENCE = 0x3fffffffffffffffL;
|
||||
private static final ProxyVoteSequence LIVE = new ProxyVoteSequence(UUID.randomUUID());
|
||||
private final long source;
|
||||
private long sequence;
|
||||
|
||||
public ProxyVoteSequence(UUID incarnation) {
|
||||
source = (incarnation.getMostSignificantBits() & 0xffffffffffff0fffL) | 0x8000L;
|
||||
}
|
||||
public static UUID nextVoteId() { return LIVE.next(); }
|
||||
public static Fence currentFence() { return LIVE.fence(); }
|
||||
public synchronized UUID next() {
|
||||
if (sequence == MAX_SEQUENCE) throw new IllegalStateException("Proxy vote sequence exhausted");
|
||||
return new UUID(source, 0x8000000000000000L | ++sequence);
|
||||
}
|
||||
public synchronized Fence fence() { return new Fence(new UUID(source, 0x8000000000000000L), sequence); }
|
||||
|
||||
public record Fence(UUID incarnation, long watermark) {
|
||||
public Fence {
|
||||
if (incarnation == null || incarnation.version() != 8 || incarnation.variant() != 2
|
||||
|| incarnation.getLeastSignificantBits() != 0x8000000000000000L
|
||||
|| watermark < 0 || watermark > MAX_SEQUENCE)
|
||||
throw new IllegalArgumentException("Invalid proxy occurrence fence");
|
||||
}
|
||||
public boolean newer(UUID occurrence) {
|
||||
return occurrence != null && occurrence.version() == 8 && occurrence.variant() == 2
|
||||
&& occurrence.getMostSignificantBits() == incarnation.getMostSignificantBits()
|
||||
&& (occurrence.getLeastSignificantBits() & MAX_SEQUENCE) > watermark;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.bencodez.votingplugin.core.session;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/** Process-local total ordering shared by guide creation and vote ingress; never a remote timestamp. */
|
||||
public final class VoteObservationSequence {
|
||||
private static final AtomicLong SEQUENCE = new AtomicLong();
|
||||
private VoteObservationSequence() { }
|
||||
public static long next() { return SEQUENCE.incrementAndGet(); }
|
||||
}
|
||||
+14
-1
@@ -12,7 +12,20 @@ import com.bencodez.votingplugin.votesites.VoteSite;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class PlayerPostVoteEvent extends Event {
|
||||
public class PlayerPostVoteEvent extends Event {
|
||||
@Getter @Setter private boolean proxySessionRealVote = true;
|
||||
/** Local recovered ingress cannot prove ordering relative to a new guide. */
|
||||
@Getter @Setter private boolean unconfirmedLocalSessionDelivery;
|
||||
|
||||
/** Backend-local ingress order forwarded by the accepted pipeline. Zero means unknown. */
|
||||
@Getter @Setter private long backendObservationOrder;
|
||||
@Getter @Setter private boolean proxyQueueClassificationKnown;
|
||||
@Getter @Setter private boolean queuedProxyVote;
|
||||
@Getter @Setter private boolean proxySessionDelivery;
|
||||
/** Guide-only provenance; independent of normal reward queue/delay policy. */
|
||||
@Getter @Setter private boolean unconfirmedProxySessionDelivery;
|
||||
/** Fresh local ingress with no supplied historical occurrence timestamp. */
|
||||
@Getter @Setter private boolean liveLocalSessionDelivery;
|
||||
|
||||
/** The Constant handlers. */
|
||||
private static final HandlerList handlers = new HandlerList();
|
||||
|
||||
@@ -20,6 +20,16 @@ import lombok.Setter;
|
||||
* Integrations must cancel before the MONITOR vote consumer runs.</p>
|
||||
*/
|
||||
public class PlayerVoteEvent extends Event implements Cancellable {
|
||||
@Getter @Setter private boolean proxySessionRealVote = true;
|
||||
|
||||
/** Backend-local strictly increasing observation, never a timestamp from another node. */
|
||||
@Getter
|
||||
@Setter
|
||||
private long backendObservationOrder = com.bencodez.votingplugin.core.session.VoteObservationSequence.next();
|
||||
/** Guide-only marker; preserves normal original vote timestamp and accounting semantics. */
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean timeQueuedLocalSessionDelivery;
|
||||
|
||||
/** The Constant handlers. */
|
||||
private static final HandlerList handlers = new HandlerList();
|
||||
@@ -96,6 +106,11 @@ public class PlayerVoteEvent extends Event implements Cancellable {
|
||||
@Setter
|
||||
private boolean proxyQueueClassificationKnown;
|
||||
|
||||
/** Guide-only provenance: original ingress is forwarded, replayed, or unknown. */
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean unconfirmedProxySessionDelivery;
|
||||
|
||||
/** Whether the proxy explicitly supplied its delay-validation decision. */
|
||||
@Getter
|
||||
@Setter
|
||||
|
||||
@@ -35,7 +35,7 @@ public class PlayerJoinEvent implements Listener {
|
||||
public void onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent event) {
|
||||
if (event == null || event.getPlayer() == null) return;
|
||||
Player player = event.getPlayer();
|
||||
plugin.getPlaceholderPlayerPresence().playerOnline(placeholderUuid(player), player);
|
||||
plugin.getPlaceholderPlayerPresence().captureOnline(placeholderUuid(player), player);
|
||||
}
|
||||
|
||||
private UUID placeholderUuid(Player player) {
|
||||
@@ -83,7 +83,7 @@ public class PlayerJoinEvent implements Listener {
|
||||
|
||||
Player player = event.getPlayer();
|
||||
if (player != null) {
|
||||
plugin.getPlaceholderPlayerPresence().playerOnline(user.getJavaUUID(), player);
|
||||
if (!plugin.getPlaceholderPlayerPresence().updateStorageUuid(user.getJavaUUID(), player)) return;
|
||||
}
|
||||
|
||||
if (player != null && plugin.isYmlError()) {
|
||||
@@ -160,17 +160,19 @@ public class PlayerJoinEvent implements Listener {
|
||||
plugin.getBackendProxyHandler().playerOffline(player.getName());
|
||||
}
|
||||
|
||||
final UUID serverUuid = player.getUniqueId();
|
||||
final String playerName = player.getName();
|
||||
plugin.getLoginTimer().execute(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
VotingPluginMain.plugin.getAdvancedTab().remove(player.getUniqueId());
|
||||
VotingPluginMain.plugin.getAdvancedTab().remove(serverUuid);
|
||||
|
||||
// Use AdvancedCore UUIDLookup to derive the correct UUID (online/offline aware)
|
||||
String uuid = UuidLookup.getInstance().getUUID(player.getName());
|
||||
String uuid = UuidLookup.getInstance().getUUID(playerName);
|
||||
if (isBlank(uuid)) {
|
||||
// Fallback to online UUID if lookup fails
|
||||
uuid = player.getUniqueId().toString();
|
||||
uuid = serverUuid.toString();
|
||||
}
|
||||
|
||||
VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(uuid);
|
||||
|
||||
+12
@@ -184,6 +184,7 @@ public class PlayerVoteListener implements Listener {
|
||||
long voteTime, UUID voteId, boolean cached) {
|
||||
PlayerPostVoteEvent post = new PlayerPostVoteEvent(site, user, event.isRealVote(), event.isForceBungee(),
|
||||
voteTime, cached, site.getServiceSite(), user.getJavaUUID(), playerName, voteId);
|
||||
copySessionObservation(post, event);
|
||||
plugin.getServer().getPluginManager().callEvent(post);
|
||||
}
|
||||
@Override public boolean placeholderCacheAlways() { return plugin.getConfigFile().getPlaceholderCacheLevel().isCacheAlways(); }
|
||||
@@ -192,6 +193,17 @@ public class PlayerVoteListener implements Listener {
|
||||
@Override public void setUpdate() { plugin.setUpdate(true); }
|
||||
}
|
||||
|
||||
static void copySessionObservation(PlayerPostVoteEvent post, PlayerVoteEvent input) {
|
||||
post.setProxySessionDelivery(input.isBungee());
|
||||
post.setProxySessionRealVote(input.isProxySessionRealVote());
|
||||
post.setUnconfirmedProxySessionDelivery(input.isUnconfirmedProxySessionDelivery());
|
||||
post.setLiveLocalSessionDelivery(!input.isBungee() && (input.getTime() == 0 || input.isTimeQueuedLocalSessionDelivery()) && input.getBackendObservationOrder() > 0);
|
||||
post.setUnconfirmedLocalSessionDelivery(!input.isBungee() && (input.getTime() == 0 || input.isTimeQueuedLocalSessionDelivery()) && input.getBackendObservationOrder() <= 0);
|
||||
post.setBackendObservationOrder(input.getBackendObservationOrder());
|
||||
post.setProxyQueueClassificationKnown(input.isProxyQueueClassificationKnown());
|
||||
post.setQueuedProxyVote(input.isQueuedProxyVote());
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
static UUID resolveProxyVoteId(PlayerVoteEvent event) {
|
||||
if (event.getProxyVoteId() != null) return event.getProxyVoteId();
|
||||
|
||||
@@ -36,6 +36,10 @@ public class VotiferEvent implements Listener {
|
||||
* @param voteUsername the validated player name
|
||||
*/
|
||||
public void processVote(String voteSite, String voteUsername) {
|
||||
processVote(voteSite, voteUsername, 0L);
|
||||
}
|
||||
/** Original process-local ingress order; zero identifies recovered or unknown ordering. */
|
||||
public void processVote(String voteSite, String voteUsername, long observationOrder) {
|
||||
try {
|
||||
plugin.getServerData().addServiceSite(voteSite);
|
||||
if (plugin.getBungeeSettings().isUseBungeecoord() && !plugin.getBungeeSettings().isVotifierBypass()
|
||||
@@ -77,7 +81,7 @@ public class VotiferEvent implements Listener {
|
||||
if (plugin.getTimeChecker().isActiveProcessing()
|
||||
&& plugin.getConfigFile().isQueueVotesDuringTimeChange()) {
|
||||
plugin.debug("Adding vote to time queue " + voteUsername + "/" + voteSite);
|
||||
plugin.getTimeQueueHandler().addVote(voteUsername, voteSite);
|
||||
plugin.getTimeQueueHandler().addVote(voteUsername, voteSite, observationOrder);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -85,6 +89,7 @@ public class VotiferEvent implements Listener {
|
||||
|
||||
PlayerVoteEvent voteEvent = new PlayerVoteEvent(
|
||||
plugin.getVoteSiteManager().getVoteSite(voteSiteName, true), voteUsername, voteSite, true);
|
||||
voteEvent.setBackendObservationOrder(observationOrder);
|
||||
plugin.getServer().getPluginManager().callEvent(voteEvent);
|
||||
|
||||
if (voteEvent.isCancelled()) {
|
||||
@@ -135,11 +140,12 @@ public class VotiferEvent implements Listener {
|
||||
plugin.debug("VoteSite: " + voteSite);
|
||||
plugin.debug("IP: " + IP);
|
||||
|
||||
final long observationOrder = com.bencodez.votingplugin.core.session.VoteObservationSequence.next();
|
||||
try {
|
||||
plugin.getVoteTimer().submit(() -> processVote(voteSite, voteUsername));
|
||||
plugin.getVoteTimer().submit(() -> processVote(voteSite, voteUsername, observationOrder));
|
||||
} catch (RejectedExecutionException rejected) {
|
||||
VotifierVoteOverflowQueue overflow = plugin.getVotifierVoteOverflowQueue();
|
||||
if (overflow == null || !overflow.enqueue(voteUsername, voteSite)) {
|
||||
if (overflow == null || !overflow.enqueue(voteUsername, voteSite, observationOrder)) {
|
||||
plugin.getLogger().severe("Votifier vote queue is full; vote was not admitted for "
|
||||
+ MinecraftUsernameValidator.sanitizeForLog(voteUsername));
|
||||
} else {
|
||||
|
||||
+19
-5
@@ -42,7 +42,9 @@ public final class VotifierVoteOverflowQueue implements AutoCloseable {
|
||||
private static final String QUEUE_FILE = "VotifierVoteQueue.yml";
|
||||
|
||||
private final VotingPluginMain plugin;
|
||||
private final BiConsumer<String, String> processor;
|
||||
@FunctionalInterface
|
||||
public interface ObservedVoteProcessor { void accept(String site, String player, long order); }
|
||||
private final ObservedVoteProcessor processor;
|
||||
private final Path file;
|
||||
private final ScheduledThreadPoolExecutor worker;
|
||||
private final Object lock = new Object();
|
||||
@@ -63,6 +65,12 @@ public final class VotifierVoteOverflowQueue implements AutoCloseable {
|
||||
* @param processor callback receiving service site and player name
|
||||
*/
|
||||
public VotifierVoteOverflowQueue(VotingPluginMain plugin, BiConsumer<String, String> processor) {
|
||||
this(plugin, (site, player, order) -> processor.accept(site, player));
|
||||
}
|
||||
public static VotifierVoteOverflowQueue withObservations(VotingPluginMain plugin, ObservedVoteProcessor processor) {
|
||||
return new VotifierVoteOverflowQueue(plugin, processor);
|
||||
}
|
||||
private VotifierVoteOverflowQueue(VotingPluginMain plugin, ObservedVoteProcessor processor) {
|
||||
this.plugin = plugin;
|
||||
this.processor = processor;
|
||||
this.file = new File(plugin.getDataFolder(), QUEUE_FILE).toPath();
|
||||
@@ -97,10 +105,13 @@ public final class VotifierVoteOverflowQueue implements AutoCloseable {
|
||||
* @return false when the bounded overflow is full or shutting down
|
||||
*/
|
||||
public boolean enqueue(String username, String serviceSite) {
|
||||
return enqueue(username, serviceSite, com.bencodez.votingplugin.core.session.VoteObservationSequence.next());
|
||||
}
|
||||
public boolean enqueue(String username, String serviceSite, long observationOrder) {
|
||||
if (username == null || serviceSite == null) return false;
|
||||
synchronized (lock) {
|
||||
if (closed || entries.size() >= MAX_ENTRIES) return false;
|
||||
entries.addLast(new PendingVote(username, serviceSite, System.currentTimeMillis()));
|
||||
entries.addLast(new PendingVote(username, serviceSite, System.currentTimeMillis(), observationOrder));
|
||||
stateVersion++;
|
||||
requestPersistenceLocked();
|
||||
scheduleDrainLocked();
|
||||
@@ -153,7 +164,7 @@ public final class VotifierVoteOverflowQueue implements AutoCloseable {
|
||||
// above cannot change in the gap before submit accepts this vote.
|
||||
plugin.getVoteTimer().submit(() -> {
|
||||
try {
|
||||
processor.accept(pending.serviceSite, pending.username);
|
||||
processor.accept(pending.serviceSite, pending.username, pending.observationOrder);
|
||||
} finally {
|
||||
acknowledge(pending);
|
||||
}
|
||||
@@ -265,7 +276,7 @@ public final class VotifierVoteOverflowQueue implements AutoCloseable {
|
||||
skipped = true;
|
||||
continue;
|
||||
}
|
||||
entries.addLast(new PendingVote(name, site, timestamp.longValue()));
|
||||
entries.addLast(new PendingVote(name, site, timestamp.longValue(), 0L));
|
||||
}
|
||||
if (skipped) {
|
||||
synchronized (lock) {
|
||||
@@ -361,7 +372,10 @@ public final class VotifierVoteOverflowQueue implements AutoCloseable {
|
||||
private final long time;
|
||||
private boolean submitted;
|
||||
|
||||
private PendingVote(String username, String serviceSite, long time) {
|
||||
// Process-local ordering is intentionally not restored across server restarts.
|
||||
private final long observationOrder;
|
||||
private PendingVote(String username, String serviceSite, long time, long observationOrder) {
|
||||
this.observationOrder = observationOrder;
|
||||
this.username = username;
|
||||
this.serviceSite = serviceSite;
|
||||
this.time = time;
|
||||
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
package com.bencodez.votingplugin.maintenance;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.bencodez.votingplugin.core.maintenance.AutoCreateVoteSitesReminderState;
|
||||
import com.bencodez.votingplugin.core.maintenance.AutoCreateVoteSitesReminderState.Candidate;
|
||||
import com.bencodez.votingplugin.core.maintenance.AutoCreateVoteSitesReminderState.Snapshot;
|
||||
import com.bencodez.votingplugin.util.DurableFiles.PublishedException;
|
||||
|
||||
/** Owner-only console reminder; all file work runs on its bounded background worker. */
|
||||
public final class AutoCreateVoteSitesReminderService {
|
||||
|
||||
public record Settings(boolean enabled, boolean autoCreateEnabled, long afterMillis, long repeatMillis) {
|
||||
public Settings {
|
||||
if (afterMillis < 0 || repeatMillis <= 0
|
||||
|| afterMillis > AutoCreateVoteSitesReminderState.MAX_INTERVAL_MILLIS
|
||||
|| repeatMillis > AutoCreateVoteSitesReminderState.MAX_INTERVAL_MILLIS) {
|
||||
throw new IllegalArgumentException("Reminder intervals must be non-negative and bounded");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface StoreFactory {
|
||||
AutoCreateVoteSitesReminderStore create(Path directory) throws IOException;
|
||||
}
|
||||
|
||||
private final Path directory;
|
||||
private final ScheduledExecutorService executor;
|
||||
private final Logger logger;
|
||||
private final LongSupplier clock;
|
||||
private final StoreFactory storeFactory;
|
||||
private final long startedNanos;
|
||||
private final AtomicBoolean started = new AtomicBoolean();
|
||||
private final AtomicBoolean recoveryRequested = new AtomicBoolean();
|
||||
private record CloseRequest(CompletableFuture<Void> result, long stoppedNanos) { }
|
||||
private final AtomicReference<CloseRequest> closing = new AtomicReference<>();
|
||||
private final Object clockGate = new Object();
|
||||
private volatile Settings settings;
|
||||
private volatile ScheduledFuture<?> task;
|
||||
// Accessed only inside the serialized worker checkpoint, never by gameplay callers.
|
||||
private AutoCreateVoteSitesReminderStore store;
|
||||
private AutoCreateVoteSitesReminderState state;
|
||||
private boolean failed;
|
||||
private boolean checkpointRequired;
|
||||
private boolean waitingReported;
|
||||
|
||||
public AutoCreateVoteSitesReminderService(Path directory, ScheduledExecutorService executor,
|
||||
Logger logger, Settings settings) {
|
||||
this(directory, executor, logger, settings, System::nanoTime, AutoCreateVoteSitesReminderStore::new);
|
||||
}
|
||||
|
||||
AutoCreateVoteSitesReminderService(Path directory, ScheduledExecutorService executor,
|
||||
Logger logger, Settings settings, LongSupplier clock, StoreFactory storeFactory) {
|
||||
this.directory = Objects.requireNonNull(directory);
|
||||
this.executor = Objects.requireNonNull(executor);
|
||||
this.logger = Objects.requireNonNull(logger);
|
||||
this.settings = Objects.requireNonNull(settings);
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
this.storeFactory = Objects.requireNonNull(storeFactory);
|
||||
startedNanos = clock.getAsLong();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (closing.get() != null || !started.compareAndSet(false, true)) return;
|
||||
try {
|
||||
task = executor.scheduleWithFixedDelay(this::tick, 0, 60, TimeUnit.SECONDS);
|
||||
if (closing.get() != null) task.cancel(false);
|
||||
} catch (RuntimeException rejected) {
|
||||
logger.warning("AutoCreateVoteSites setup reminder could not start; voting is unaffected.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Reload is a snapshot update, never synchronous file I/O or another periodic task. */
|
||||
public void reload(Settings next) {
|
||||
settings = Objects.requireNonNull(next);
|
||||
recoveryRequested.set(true);
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
long nowNanos;
|
||||
synchronized (clockGate) {
|
||||
if (closing.get() != null) return;
|
||||
nowNanos = clock.getAsLong();
|
||||
}
|
||||
try {
|
||||
checkpoint(nowNanos, false);
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
logger.warning("AutoCreateVoteSites setup reminder paused after a checkpoint failure; "
|
||||
+ "repair its checkpoint if necessary and reload or restart. Voting is unaffected.");
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void checkpoint(long nowNanos, boolean finalCheckpoint) throws IOException {
|
||||
CloseRequest stopped = closing.get();
|
||||
if (stopped != null) {
|
||||
if (!finalCheckpoint) return;
|
||||
nowNanos = stopped.stoppedNanos();
|
||||
}
|
||||
if (failed && !recoveryRequested.getAndSet(false)) {
|
||||
if (finalCheckpoint) throw new IOException("Reminder checkpoint remains unavailable");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (store == null) store = storeFactory.create(directory);
|
||||
if (!store.tryAcquireLease()) {
|
||||
if (finalCheckpoint) throw new IOException("Final reminder checkpoint has no storage ownership");
|
||||
if (!waitingReported) {
|
||||
logger.warning("AutoCreateVoteSites setup reminder is waiting for a retiring reminder worker; "
|
||||
+ "voting is unaffected.");
|
||||
waitingReported = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
waitingReported = false;
|
||||
if (state == null || failed) {
|
||||
Snapshot saved;
|
||||
try {
|
||||
saved = store.load();
|
||||
} catch (IOException | RuntimeException unverified) {
|
||||
checkpointRequired = true;
|
||||
throw unverified;
|
||||
}
|
||||
if (saved == null && checkpointRequired) {
|
||||
throw new IOException("Reminder checkpoint was removed while running");
|
||||
}
|
||||
if (saved != null) checkpointRequired = true;
|
||||
state = new AutoCreateVoteSitesReminderState(saved == null ? new Snapshot(0, -1) : saved,
|
||||
state == null && !failed ? startedNanos : nowNanos);
|
||||
failed = false;
|
||||
} else if (checkpointRequired && store.load() == null) {
|
||||
throw new IOException("Reminder checkpoint was removed while running");
|
||||
}
|
||||
recoveryRequested.set(false);
|
||||
Settings current = settings;
|
||||
Candidate candidate = state.propose(nowNanos, !finalCheckpoint && current.enabled(),
|
||||
current.autoCreateEnabled(), current.afterMillis(), current.repeatMillis());
|
||||
try {
|
||||
store.save(candidate.snapshot());
|
||||
} catch (PublishedException uncertain) {
|
||||
checkpointRequired = true;
|
||||
throw uncertain;
|
||||
}
|
||||
checkpointRequired = true;
|
||||
state.commit(candidate);
|
||||
// Do not publish a stale warning if shutdown or configuration changed during I/O.
|
||||
if (candidate.reminderDue() && closing.get() == null && current.equals(settings)) {
|
||||
logger.warning("After voting-site setup, set AutoCreateVoteSites: false in Config.yml.");
|
||||
}
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
failed = true;
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
/** Cancel promptly; enqueue the final checkpoint without blocking the server thread. */
|
||||
public CompletableFuture<Void> closeAsync() {
|
||||
CloseRequest request;
|
||||
synchronized (clockGate) {
|
||||
CloseRequest existing = closing.get();
|
||||
if (existing != null) return existing.result();
|
||||
request = new CloseRequest(new CompletableFuture<>(), clock.getAsLong());
|
||||
closing.set(request);
|
||||
}
|
||||
CompletableFuture<Void> result = request.result();
|
||||
long stoppedNanos = request.stoppedNanos();
|
||||
ScheduledFuture<?> current = task;
|
||||
if (current != null) current.cancel(false);
|
||||
if (!started.get()) {
|
||||
result.complete(null);
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
executor.execute(() -> {
|
||||
Exception failure = null;
|
||||
try {
|
||||
checkpoint(stoppedNanos, true);
|
||||
} catch (IOException | RuntimeException checkpointFailure) {
|
||||
failure = checkpointFailure;
|
||||
} finally {
|
||||
if (store != null) {
|
||||
try {
|
||||
store.releaseLease();
|
||||
} catch (IOException | RuntimeException cleanupFailure) {
|
||||
if (failure == null) failure = cleanupFailure;
|
||||
else failure.addSuppressed(cleanupFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure == null) result.complete(null);
|
||||
else {
|
||||
logger.warning("AutoCreateVoteSites reminder checkpoint or cleanup did not complete; "
|
||||
+ "inspect its storage before reloading.");
|
||||
result.completeExceptionally(failure);
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException rejected) {
|
||||
result.completeExceptionally(rejected);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
package com.bencodez.votingplugin.maintenance;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.channels.OverlappingFileLockException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.OpenOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.PosixFileAttributeView;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import com.bencodez.votingplugin.core.maintenance.AutoCreateVoteSitesReminderState.Snapshot;
|
||||
import com.bencodez.votingplugin.util.DurableFiles;
|
||||
|
||||
/**
|
||||
* A fixed, checksummed active-runtime checkpoint in the plugin's existing data
|
||||
* directory. One reminder worker owns all load/save operations. This store never
|
||||
* creates the directory, repairs corrupt records, or follows a record symlink.
|
||||
*/
|
||||
public final class AutoCreateVoteSitesReminderStore {
|
||||
|
||||
private static final String FILE_NAME = ".auto-create-sites-reminder.dat";
|
||||
private static final byte[] MAGIC = "VPACR001".getBytes(StandardCharsets.US_ASCII);
|
||||
private static final int HEADER_BYTES = 24;
|
||||
private static final int RECORD_BYTES = 56;
|
||||
private final Path directory;
|
||||
private final Path target;
|
||||
private final Publisher publisher;
|
||||
private FileChannel leaseChannel;
|
||||
private FileLock lease;
|
||||
|
||||
@FunctionalInterface
|
||||
interface Publisher {
|
||||
void publish(Path staged, Path target) throws IOException;
|
||||
}
|
||||
|
||||
public AutoCreateVoteSitesReminderStore(Path dataDirectory) throws IOException {
|
||||
this(dataDirectory, DurableFiles::publishStagedFile);
|
||||
}
|
||||
|
||||
AutoCreateVoteSitesReminderStore(Path dataDirectory, Publisher publisher) throws IOException {
|
||||
this.directory = Objects.requireNonNull(dataDirectory, "dataDirectory").toAbsolutePath().normalize();
|
||||
this.target = directory.resolve(FILE_NAME);
|
||||
this.publisher = Objects.requireNonNull(publisher, "publisher");
|
||||
validateDirectory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Nonblocking admission across plugin instances/class loaders in one JVM, held
|
||||
* through final publication. Multiple JVMs sharing the directory are unsupported:
|
||||
* closing another channel may release native locks on some platforms.
|
||||
*/
|
||||
public boolean tryAcquireLease() throws IOException {
|
||||
if (lease != null && lease.isValid()) return true;
|
||||
validateDirectory();
|
||||
Path lockPath = directory.resolve(".auto-create-sites-reminder.lock");
|
||||
try {
|
||||
BasicFileAttributes attributes = Files.readAttributes(lockPath, BasicFileAttributes.class,
|
||||
LinkOption.NOFOLLOW_LINKS);
|
||||
if (!attributes.isRegularFile() || attributes.isSymbolicLink() || attributes.size() != 0) {
|
||||
throw new IOException("Invalid reminder ownership file");
|
||||
}
|
||||
} catch (NoSuchFileException absent) {
|
||||
// Create only this fixed empty metadata file inside the checked data directory.
|
||||
}
|
||||
Set<OpenOption> options = Set.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE,
|
||||
LinkOption.NOFOLLOW_LINKS);
|
||||
FileChannel channel = Files.getFileAttributeView(directory, PosixFileAttributeView.class,
|
||||
LinkOption.NOFOLLOW_LINKS) == null ? FileChannel.open(lockPath, options)
|
||||
: FileChannel.open(lockPath, options,
|
||||
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")));
|
||||
boolean acquired = false;
|
||||
try {
|
||||
if (channel.size() != 0) throw new IOException("Invalid reminder ownership file");
|
||||
FileLock candidate;
|
||||
try {
|
||||
candidate = channel.tryLock();
|
||||
} catch (OverlappingFileLockException busy) {
|
||||
return false;
|
||||
}
|
||||
if (candidate == null) return false;
|
||||
leaseChannel = channel;
|
||||
lease = candidate;
|
||||
acquired = true;
|
||||
return true;
|
||||
} finally {
|
||||
if (!acquired) channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** Closing the channel releases its lock; keep the fixed lock file for successors. */
|
||||
public void releaseLease() throws IOException {
|
||||
FileChannel channel = leaseChannel;
|
||||
leaseChannel = null;
|
||||
lease = null;
|
||||
if (channel != null) channel.close();
|
||||
}
|
||||
|
||||
/** Returns null only when the record is absent from a valid existing directory. */
|
||||
public Snapshot load() throws IOException {
|
||||
validateDirectory();
|
||||
try {
|
||||
return readRecord(target);
|
||||
} catch (NoSuchFileException absent) {
|
||||
// A missing/replaced data directory is a failure, not an absent checkpoint.
|
||||
validateDirectory();
|
||||
if (Files.notExists(target, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return null;
|
||||
}
|
||||
throw absent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a checked same-directory staged record using DurableFiles. Existing
|
||||
* invalid or unreadable records block replacement. PublishedException propagates
|
||||
* unchanged: the new record may already be visible, so no rollback is attempted.
|
||||
*/
|
||||
public void save(Snapshot snapshot) throws IOException {
|
||||
Objects.requireNonNull(snapshot, "snapshot");
|
||||
load();
|
||||
Path staged = createStagedFile();
|
||||
Throwable failure = null;
|
||||
try {
|
||||
ByteBuffer record = ByteBuffer.wrap(encode(snapshot));
|
||||
try (FileChannel channel = FileChannel.open(staged, StandardOpenOption.WRITE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING, LinkOption.NOFOLLOW_LINKS)) {
|
||||
while (record.hasRemaining()) {
|
||||
if (channel.write(record) <= 0) {
|
||||
throw new IOException("Could not write reminder checkpoint");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!snapshot.equals(readRecord(staged))) {
|
||||
throw new IOException("Staged reminder checkpoint did not match");
|
||||
}
|
||||
// Recheck after staging, before replacing any existing destination.
|
||||
load();
|
||||
publisher.publish(staged, target);
|
||||
} catch (IOException | RuntimeException | Error primary) {
|
||||
failure = primary;
|
||||
throw primary;
|
||||
} finally {
|
||||
try {
|
||||
Files.deleteIfExists(staged);
|
||||
} catch (IOException | RuntimeException cleanup) {
|
||||
if (failure != null) {
|
||||
failure.addSuppressed(cleanup);
|
||||
} else {
|
||||
throw new DurableFiles.PublishedException(cleanup instanceof IOException io ? io
|
||||
: new IOException("Reminder staging cleanup failed after publication", cleanup));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDirectory() throws IOException {
|
||||
BasicFileAttributes attributes = Files.readAttributes(directory, BasicFileAttributes.class,
|
||||
LinkOption.NOFOLLOW_LINKS);
|
||||
if (!attributes.isDirectory() || attributes.isSymbolicLink()) {
|
||||
throw new IOException("Reminder data directory must be an existing directory");
|
||||
}
|
||||
}
|
||||
|
||||
private Path createStagedFile() throws IOException {
|
||||
if (Files.getFileAttributeView(directory, PosixFileAttributeView.class, LinkOption.NOFOLLOW_LINKS) != null) {
|
||||
return Files.createTempFile(directory, ".auto-create-sites-reminder-", ".tmp",
|
||||
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")));
|
||||
}
|
||||
return Files.createTempFile(directory, ".auto-create-sites-reminder-", ".tmp");
|
||||
}
|
||||
|
||||
private static Snapshot readRecord(Path recordPath) throws IOException {
|
||||
BasicFileAttributes attributes = Files.readAttributes(recordPath, BasicFileAttributes.class,
|
||||
LinkOption.NOFOLLOW_LINKS);
|
||||
if (!attributes.isRegularFile() || attributes.isSymbolicLink() || attributes.size() != RECORD_BYTES) {
|
||||
throw new IOException("Invalid reminder checkpoint file");
|
||||
}
|
||||
ByteBuffer record = ByteBuffer.allocate(RECORD_BYTES);
|
||||
try (FileChannel channel = FileChannel.open(recordPath, StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS)) {
|
||||
if (channel.size() != RECORD_BYTES) {
|
||||
throw new IOException("Invalid reminder checkpoint length");
|
||||
}
|
||||
while (record.hasRemaining()) {
|
||||
if (channel.read(record) <= 0) {
|
||||
throw new IOException("Incomplete reminder checkpoint");
|
||||
}
|
||||
}
|
||||
if (channel.size() != RECORD_BYTES || channel.read(ByteBuffer.allocate(1)) != -1) {
|
||||
throw new IOException("Trailing reminder checkpoint data");
|
||||
}
|
||||
}
|
||||
return decode(record.array());
|
||||
}
|
||||
|
||||
private static byte[] encode(Snapshot snapshot) {
|
||||
ByteBuffer record = ByteBuffer.allocate(RECORD_BYTES).order(ByteOrder.BIG_ENDIAN);
|
||||
record.put(MAGIC).putLong(snapshot.totalRuntimeMillis()).putLong(snapshot.lastReminderRuntimeMillis());
|
||||
record.put(digest(Arrays.copyOf(record.array(), HEADER_BYTES)));
|
||||
return record.array();
|
||||
}
|
||||
|
||||
private static Snapshot decode(byte[] record) throws IOException {
|
||||
if (!Arrays.equals(MAGIC, Arrays.copyOf(record, MAGIC.length))) {
|
||||
throw new IOException("Invalid reminder checkpoint schema");
|
||||
}
|
||||
byte[] expected = digest(Arrays.copyOf(record, HEADER_BYTES));
|
||||
if (!MessageDigest.isEqual(expected, Arrays.copyOfRange(record, HEADER_BYTES, RECORD_BYTES))) {
|
||||
throw new IOException("Invalid reminder checkpoint checksum");
|
||||
}
|
||||
ByteBuffer header = ByteBuffer.wrap(record).order(ByteOrder.BIG_ENDIAN);
|
||||
header.position(MAGIC.length);
|
||||
try {
|
||||
return new Snapshot(header.getLong(), header.getLong());
|
||||
} catch (IllegalArgumentException invalid) {
|
||||
throw new IOException("Invalid reminder checkpoint counters", invalid);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] digest(byte[] header) {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-256").digest(header);
|
||||
} catch (NoSuchAlgorithmException unavailable) {
|
||||
throw new IllegalStateException("SHA-256 is unavailable", unavailable);
|
||||
}
|
||||
}
|
||||
}
|
||||
+106
-2
@@ -7,6 +7,8 @@ import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -19,6 +21,97 @@ import org.bukkit.entity.Player;
|
||||
public final class PlaceholderPlayerPresence {
|
||||
private final AtomicReference<Map<UUID, Player>> onlinePlayers = new AtomicReference<>(Map.of());
|
||||
private final Object lifecycleLock = new Object();
|
||||
private final Map<Player, LiveIdentity> identities = new IdentityHashMap<>();
|
||||
private final Map<Player, Object> refreshReservations = new IdentityHashMap<>();
|
||||
|
||||
/** Immutable identity; lookup callers never need to access its Bukkit owner. */
|
||||
public record LiveIdentity(UUID serverUuid, String playerName, UUID storageUuid) { }
|
||||
|
||||
/** Capture only on this player's owning thread (join or entity scheduler). */
|
||||
public void captureOnline(UUID storageUuid, Player player) {
|
||||
if (storageUuid == null || player == null) return;
|
||||
LiveIdentity identity = new LiveIdentity(player.getUniqueId(), player.getName(), storageUuid);
|
||||
synchronized (lifecycleLock) {
|
||||
playerOnline(storageUuid, player);
|
||||
identities.put(player, identity);
|
||||
}
|
||||
}
|
||||
|
||||
/** Rekey only the current owner. Delayed login cannot resurrect a retired session. */
|
||||
public boolean updateStorageUuid(UUID uuid, Player player) {
|
||||
if (uuid == null || player == null) return false;
|
||||
synchronized (lifecycleLock) {
|
||||
if (storageUuid(player) == null) return false;
|
||||
Player currentOwner = onlinePlayers.get().get(uuid);
|
||||
if (currentOwner != null && currentOwner != player) return false;
|
||||
LiveIdentity identity = identities.get(player);
|
||||
playerOnline(uuid, player);
|
||||
if (identity != null) identities.put(player,
|
||||
new LiveIdentity(identity.serverUuid(), identity.playerName(), uuid));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Snapshot-only lookup, with exact storage identities taking precedence over aliases. */
|
||||
public LiveIdentity liveIdentity(UUID uuid) {
|
||||
if (uuid == null) return null;
|
||||
synchronized (lifecycleLock) {
|
||||
Player exact = onlinePlayers.get().get(uuid);
|
||||
if (exact != null) return identities.get(exact);
|
||||
LiveIdentity found = null;
|
||||
for (LiveIdentity identity : identities.values()) {
|
||||
if (!uuid.equals(identity.serverUuid())) continue;
|
||||
if (found != null) return null; // Uncertain or ambiguous: preserve the supplied UUID.
|
||||
found = identity;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh membership without reading another region's players. Each scheduled
|
||||
* capture is fenced by its reservation; quit, join, another refresh and clear
|
||||
* invalidate it before it can publish stale identity data. The capture consumer
|
||||
* must run on that player's owning thread.
|
||||
*/
|
||||
public void refresh(Supplier<? extends Collection<? extends Player>> playersSupplier,
|
||||
BiConsumer<Player, Consumer<UUID>> scheduleCapture) {
|
||||
Map<Player, Object> reservations = new IdentityHashMap<>();
|
||||
synchronized (lifecycleLock) {
|
||||
Collection<? extends Player> players = playersSupplier.get();
|
||||
Map<Player, Boolean> members = new IdentityHashMap<>();
|
||||
if (players != null) for (Player player : players) {
|
||||
if (player != null) members.put(player, Boolean.TRUE);
|
||||
}
|
||||
Map<UUID, Player> retained = new HashMap<>(onlinePlayers.get());
|
||||
retained.entrySet().removeIf(entry -> !members.containsKey(entry.getValue()));
|
||||
onlinePlayers.set(Map.copyOf(retained));
|
||||
retainCurrentIdentities();
|
||||
refreshReservations.clear();
|
||||
members.keySet().forEach(player -> {
|
||||
Object reservation = new Object();
|
||||
refreshReservations.put(player, reservation);
|
||||
reservations.put(player, reservation);
|
||||
});
|
||||
}
|
||||
reservations.forEach((player, reservation) -> scheduleCapture.accept(player, storageUuid -> {
|
||||
synchronized (lifecycleLock) {
|
||||
if (refreshReservations.get(player) != reservation) return;
|
||||
refreshReservations.remove(player);
|
||||
if (storageUuid == null || !player.isOnline()) return;
|
||||
Player currentOwner = onlinePlayers.get().get(storageUuid);
|
||||
if (currentOwner != null && currentOwner != player) return;
|
||||
UUID serverUuid = player.getUniqueId();
|
||||
if (identities.entrySet().stream().anyMatch(entry -> entry.getKey() != player
|
||||
&& serverUuid.equals(entry.getValue().serverUuid()))) return;
|
||||
captureOnline(storageUuid, player);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void retainCurrentIdentities() {
|
||||
identities.entrySet().removeIf(entry -> onlinePlayers.get().get(entry.getValue().storageUuid()) != entry.getKey());
|
||||
}
|
||||
|
||||
public boolean isOnline(UUID uuid) {
|
||||
return uuid != null && onlinePlayers.get().containsKey(uuid);
|
||||
@@ -46,24 +139,31 @@ public final class PlaceholderPlayerPresence {
|
||||
public void playerOnline(UUID uuid, Player player) {
|
||||
if (uuid == null || player == null) return;
|
||||
synchronized (lifecycleLock) {
|
||||
refreshReservations.remove(player);
|
||||
Player displacedOwner = onlinePlayers.get().get(uuid);
|
||||
if (displacedOwner != null && displacedOwner != player) refreshReservations.remove(displacedOwner);
|
||||
update(current -> {
|
||||
Map<UUID, Player> next = new HashMap<>(current);
|
||||
next.entrySet().removeIf(entry -> entry.getValue() == player && !entry.getKey().equals(uuid));
|
||||
next.put(uuid, player);
|
||||
return Map.copyOf(next);
|
||||
});
|
||||
retainCurrentIdentities();
|
||||
}
|
||||
}
|
||||
|
||||
public void playerOffline(UUID uuid) {
|
||||
if (uuid == null) return;
|
||||
synchronized (lifecycleLock) {
|
||||
Player owner = onlinePlayers.get().get(uuid);
|
||||
if (owner != null) refreshReservations.remove(owner);
|
||||
update(current -> {
|
||||
if (!current.containsKey(uuid)) return current;
|
||||
Map<UUID, Player> next = new HashMap<>(current);
|
||||
next.remove(uuid);
|
||||
return Map.copyOf(next);
|
||||
});
|
||||
retainCurrentIdentities();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,11 +171,13 @@ public final class PlaceholderPlayerPresence {
|
||||
public boolean playerOffline(UUID uuid, Player expectedOwner) {
|
||||
if (uuid == null || expectedOwner == null) return false;
|
||||
synchronized (lifecycleLock) {
|
||||
refreshReservations.remove(expectedOwner);
|
||||
Map<UUID, Player> current = onlinePlayers.get();
|
||||
if (current.get(uuid) != expectedOwner) return false;
|
||||
Map<UUID, Player> next = new HashMap<>(current);
|
||||
next.remove(uuid);
|
||||
onlinePlayers.set(Map.copyOf(next));
|
||||
retainCurrentIdentities();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -113,6 +215,8 @@ public final class PlaceholderPlayerPresence {
|
||||
}
|
||||
}
|
||||
onlinePlayers.set(Map.copyOf(next));
|
||||
refreshReservations.clear();
|
||||
retainCurrentIdentities();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,11 +226,11 @@ public final class PlaceholderPlayerPresence {
|
||||
if (players != null) players.forEach((uuid, player) -> {
|
||||
if (uuid != null && player != null) next.put(uuid, player);
|
||||
});
|
||||
synchronized (lifecycleLock) { onlinePlayers.set(Map.copyOf(next)); }
|
||||
synchronized (lifecycleLock) { onlinePlayers.set(Map.copyOf(next)); refreshReservations.clear(); retainCurrentIdentities(); }
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
synchronized (lifecycleLock) { onlinePlayers.set(Map.of()); }
|
||||
synchronized (lifecycleLock) { onlinePlayers.set(Map.of()); identities.clear(); refreshReservations.clear(); }
|
||||
}
|
||||
|
||||
/** Publish an offline-only result atomically with player lifecycle capture. */
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ public final class PendingIncomingVoteQueue {
|
||||
|
||||
public synchronized PendingIncomingVote admit(String player, String service) {
|
||||
if (!accepting || votes.size() >= MAX_PENDING) return null;
|
||||
PendingIncomingVote vote = new PendingIncomingVote(UUID.randomUUID(), player, service,
|
||||
PendingIncomingVote vote = new PendingIncomingVote(com.bencodez.votingplugin.core.session.ProxyVoteSequence.nextVoteId(), player, service,
|
||||
System.currentTimeMillis());
|
||||
votes.put(vote.getVoteId(), vote);
|
||||
return vote;
|
||||
|
||||
+11
-1
@@ -74,6 +74,7 @@ final class ReliableVoteDeliveryOutbox {
|
||||
long deferredCount = entries.values().stream().filter(Entry::deferredUpdate).count();
|
||||
if (deferred ? !update || deferredCount >= MAX_DEFERRED_UPDATE_ENTRIES
|
||||
: update ? readyUpdates() >= MAX_UPDATE_ENTRIES : entries.size() - updates >= MAX_ENTRIES) return false;
|
||||
envelope = retryEnvelope(envelope);
|
||||
String record = addRecord(server, envelope, deferred);
|
||||
Entry entry = new Entry(server, envelope, false, false, false, deferred);
|
||||
long remainingReserve = terminalRecordReserve() + terminalRecordReserve(entry);
|
||||
@@ -238,7 +239,7 @@ final class ReliableVoteDeliveryOutbox {
|
||||
try {
|
||||
if (parts.length == 3 && (ADD.equals(parts[0]) || DEFERRED.equals(parts[0]))) {
|
||||
String server = decode(parts[1]);
|
||||
JsonEnvelope envelope = JsonEnvelopeCodec.decode(decode(parts[2]));
|
||||
JsonEnvelope envelope = retryEnvelope(JsonEnvelopeCodec.decode(decode(parts[2])));
|
||||
String key = key(server, envelope);
|
||||
if (key == null) throw new IllegalArgumentException("Invalid vote envelope");
|
||||
boolean deferred = DEFERRED.equals(parts[0]);
|
||||
@@ -395,6 +396,15 @@ final class ReliableVoteDeliveryOutbox {
|
||||
return value.getBytes(StandardCharsets.UTF_8).length;
|
||||
}
|
||||
|
||||
/** A retained delivery is no longer an immediate live observation, including pre-upgrade rows. */
|
||||
private static JsonEnvelope retryEnvelope(JsonEnvelope envelope) {
|
||||
if (!VotingPluginWire.SUB_VOTE.equals(envelope.getSubChannel())
|
||||
&& !VotingPluginWire.SUB_VOTE_ONLINE.equals(envelope.getSubChannel())) return envelope;
|
||||
var copy = JsonEnvelope.builder(envelope.getSubChannel()).schema(envelope.getSchema());
|
||||
envelope.getFields().forEach(copy::put);
|
||||
return copy.put(VotingPluginWire.K_SESSION_DELIVERY_FRESH, false).build();
|
||||
}
|
||||
|
||||
private static String key(String server, JsonEnvelope envelope) {
|
||||
if (server == null || server.isBlank() || envelope == null) return null;
|
||||
String subChannel = envelope.getSubChannel();
|
||||
|
||||
@@ -2253,7 +2253,10 @@ public abstract class VotingPluginProxy {
|
||||
votePartyVotes = getVoteCacheCurrentVotePartyVotes();
|
||||
|
||||
globalMessageProxyHandler = new VotingPluginGlobalMessageProxyHandler();
|
||||
registerControlEnrollmentListener(globalMessageProxyHandler);
|
||||
registerControlEnrollmentListener(globalMessageProxyHandler);
|
||||
globalMessageProxyHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_SESSION_FENCE_REQUEST) {
|
||||
@Override public void onReceive(JsonEnvelope message) { handleSessionFenceRequest(message); }
|
||||
});
|
||||
|
||||
globalMessageProxyHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_LOGIN) {
|
||||
@Override
|
||||
@@ -2744,7 +2747,14 @@ public abstract class VotingPluginProxy {
|
||||
// encryption policy during a soft reload.
|
||||
SharedTransportEnvelopeAuthenticator multiProxyAuthenticator = getConfig().getMultiProxySupport()
|
||||
? sharedTransportAuthenticator() : sharedTransportAuthenticator;
|
||||
multiProxyHandler = new MultiProxyHandler() {
|
||||
multiProxyHandler = new MultiProxyHandler() {
|
||||
@Override protected void onSessionFenceReply(JsonEnvelope message) {
|
||||
var reply = VotingPluginWire.readSessionFenceReply(message);
|
||||
if (reply != null && reply.request().origin().equals(getConfig().getProxyServerName())
|
||||
&& isServerValid(reply.request().server()))
|
||||
globalMessageProxyHandler.sendMessage(reply.request().server(), 0, message);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void addNonVotedPlayerCache(String uuid, String player) {
|
||||
@@ -2895,6 +2905,8 @@ public abstract class VotingPluginProxy {
|
||||
@Override
|
||||
public void triggerVote(String player, String service, boolean realVote, boolean timeQueue, long queueTime,
|
||||
VoteTotalsSnapshot text, String uuid) {
|
||||
// Legacy callbacks have no ACK/retirement owner. Preserve their ordinary
|
||||
// delivery path rather than creating permanent multi-proxy completion files.
|
||||
vote(player, service, realVote, timeQueue, queueTime, text, uuid);
|
||||
}
|
||||
|
||||
@@ -5992,6 +6004,18 @@ public abstract class VotingPluginProxy {
|
||||
return sendProxyBroadcastEnvelopeNow(server, envelope);
|
||||
}
|
||||
|
||||
/** Read-only session negotiation uses existing encrypted/authenticated transports. */
|
||||
protected void handleSessionFenceRequest(JsonEnvelope message) {
|
||||
var request = VotingPluginWire.readSessionFenceRequest(message);
|
||||
if (request == null || !request.origin().isEmpty() || !isServerValid(request.server())) return;
|
||||
var fence = com.bencodez.votingplugin.core.session.ProxyVoteSequence.currentFence();
|
||||
globalMessageProxyHandler.sendMessage(request.server(), 0, VotingPluginWire.sessionFenceReply(request, fence));
|
||||
if (multiProxyHandler != null && getConfig().getMultiProxySupport()) {
|
||||
var relayed = new VotingPluginWire.SessionFenceRequest(request.player(), request.nonce(),
|
||||
request.server(), getConfig().getProxyServerName());
|
||||
multiProxyHandler.sendMultiProxyEnvelopeAccepted(VotingPluginWire.sessionFenceRequest(relayed));
|
||||
}
|
||||
}
|
||||
protected void handleStatusOkay(JsonEnvelope message) {
|
||||
String server = message.getFields().getOrDefault(VotingPluginWire.K_SERVER, "");
|
||||
String request = message.getFields().getOrDefault(VotingPluginWire.K_REQUEST_ID, "");
|
||||
@@ -6049,6 +6073,14 @@ public abstract class VotingPluginProxy {
|
||||
return sendVoteEnvelopeAccepted(playerServer, 1, envelope);
|
||||
}
|
||||
|
||||
/** Backend ingress cannot establish original freshness after a forwarding hop or replay. */
|
||||
private JsonEnvelope withSessionDeliveryProvenance(JsonEnvelope envelope, UUID voteId, VoteTimeQueue queuedVote) {
|
||||
MultiProxyVoteRetry forwarded = multiProxyVoteRetries.get(voteId);
|
||||
if (forwarded != null) return VotingPluginWire.unconfirmedSessionDelivery(envelope, forwarded.origin);
|
||||
if (queuedVote != null) return VotingPluginWire.unconfirmedSessionDelivery(envelope, queuedVote.getMultiProxyOrigin());
|
||||
return envelope;
|
||||
}
|
||||
|
||||
private JsonEnvelope cachedVoteEnvelope(OfflineBungeeVote vote, boolean online, boolean broadcast,
|
||||
int num, int numberOfVotes) {
|
||||
if (vote.hasCachedVoteBatch()) {
|
||||
@@ -6073,6 +6105,9 @@ public abstract class VotingPluginProxy {
|
||||
resolveCachedWasOnline(vote), vote.isRealVote(), vote.getText(), vote.getVoteId(),
|
||||
getConfig().getBungeeManageTotals(), broadcast, num, numberOfVotes);
|
||||
}
|
||||
// Cached delivery cannot prove fresh original ingress in the backend's clock
|
||||
// domain, even when the reward policy correctly classifies it as non-queued.
|
||||
envelope = VotingPluginWire.unconfirmedSessionDelivery(envelope, "");
|
||||
// A cache entry is only known to have passed the proxy's delay gate when
|
||||
// that decision was persisted with it. Unknown legacy entries must still
|
||||
// be checked by the receiving backend.
|
||||
@@ -6851,6 +6886,7 @@ public abstract class VotingPluginProxy {
|
||||
authoritativeDelayValidated)
|
||||
: VotingPluginWire.vote(player, uuid, service, time, playerOnline, realVote, text.toString(),
|
||||
voteId, getConfig().getBungeeManageTotals(), broadcastHere, 1, 1);
|
||||
rewardEnvelope = withSessionDeliveryProvenance(rewardEnvelope, voteId, queuedVote);
|
||||
boolean rewardAccepted = sendVoteEnvelopeAccepted(s, 2, rewardEnvelope, pendingVote);
|
||||
if (!rewardAccepted) {
|
||||
pendingVote.setRewardDelivered(false);
|
||||
@@ -6910,6 +6946,7 @@ public abstract class VotingPluginProxy {
|
||||
authoritativeDelayValidated)
|
||||
: VotingPluginWire.voteOnline(player, uuid, service, time, playerOnline, realVote,
|
||||
text.toString(), voteId, getConfig().getBungeeManageTotals(), broadcastHere, 1, 1);
|
||||
rewardEnvelope = withSessionDeliveryProvenance(rewardEnvelope, voteId, queuedVote);
|
||||
rewardAccepted = sendVoteEnvelopeAccepted(server, 1, rewardEnvelope, pendingVote);
|
||||
if (rewardAccepted) {
|
||||
retryState.deliveredRewardServers.add(server);
|
||||
|
||||
@@ -35,10 +35,59 @@ public final class VotingPluginWire {
|
||||
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
/** Additive request/reply negotiation; old peers simply ignore these subchannels. */
|
||||
public record SessionFenceRequest(UUID player, UUID nonce, String server, String origin) { }
|
||||
public record SessionFenceReply(SessionFenceRequest request,
|
||||
com.bencodez.votingplugin.core.session.ProxyVoteSequence.Fence fence) { }
|
||||
public static JsonEnvelope sessionFenceRequest(SessionFenceRequest request) {
|
||||
return sessionFenceBuilder(SUB_VOTE_SESSION_FENCE_REQUEST, request).build();
|
||||
}
|
||||
public static JsonEnvelope sessionFenceReply(SessionFenceRequest request,
|
||||
com.bencodez.votingplugin.core.session.ProxyVoteSequence.Fence fence) {
|
||||
return sessionFenceBuilder(SUB_VOTE_SESSION_FENCE_REPLY, request)
|
||||
.put(K_SESSION_INCARNATION, fence.incarnation().toString())
|
||||
.put(K_SESSION_WATERMARK, Long.toString(fence.watermark())).build();
|
||||
}
|
||||
private static JsonEnvelope.Builder sessionFenceBuilder(String sub, SessionFenceRequest request) {
|
||||
return base(sub).put(K_SESSION_FENCE_VERSION, 1).put(K_UUID, request.player().toString())
|
||||
.put(K_SESSION_NONCE, request.nonce().toString()).put(K_SERVER, request.server())
|
||||
.put(K_SESSION_ORIGIN, request.origin());
|
||||
}
|
||||
public static SessionFenceRequest readSessionFenceRequest(JsonEnvelope message) {
|
||||
if (message == null || message.getSchema() != SCHEMA_VERSION
|
||||
|| !(SUB_VOTE_SESSION_FENCE_REQUEST.equals(message.getSubChannel())
|
||||
|| SUB_VOTE_SESSION_FENCE_REPLY.equals(message.getSubChannel()))
|
||||
|| !"1".equals(message.getFields().get(K_SESSION_FENCE_VERSION))) return null;
|
||||
var fields = message.getFields();
|
||||
try {
|
||||
String server = fields.getOrDefault(K_SERVER, "");
|
||||
String origin = fields.getOrDefault(K_SESSION_ORIGIN, "");
|
||||
if (server.isBlank() || server.length() > 128 || origin.length() > 128) return null;
|
||||
return new SessionFenceRequest(UUID.fromString(fields.get(K_UUID)),
|
||||
UUID.fromString(fields.get(K_SESSION_NONCE)), server, origin);
|
||||
} catch (IllegalArgumentException | NullPointerException invalid) { return null; }
|
||||
}
|
||||
public static SessionFenceReply readSessionFenceReply(JsonEnvelope message) {
|
||||
SessionFenceRequest request = readSessionFenceRequest(message);
|
||||
if (request == null || !SUB_VOTE_SESSION_FENCE_REPLY.equals(message.getSubChannel())) return null;
|
||||
try {
|
||||
return new SessionFenceReply(request, new com.bencodez.votingplugin.core.session.ProxyVoteSequence.Fence(
|
||||
UUID.fromString(message.getFields().get(K_SESSION_INCARNATION)),
|
||||
Long.parseLong(message.getFields().get(K_SESSION_WATERMARK))));
|
||||
} catch (IllegalArgumentException | NullPointerException invalid) { return null; }
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Subchannels (canonical)
|
||||
// =========================
|
||||
public static final String SUB_VOTE = "Vote";
|
||||
public static final String SUB_VOTE_SESSION_FENCE_REQUEST = "VoteSessionFenceRequest";
|
||||
public static final String SUB_VOTE_SESSION_FENCE_REPLY = "VoteSessionFenceReply";
|
||||
public static final String K_SESSION_FENCE_VERSION = "sessionFenceVersion";
|
||||
public static final String K_SESSION_NONCE = "sessionNonce";
|
||||
public static final String K_SESSION_ORIGIN = "sessionOrigin";
|
||||
public static final String K_SESSION_INCARNATION = "sessionIncarnation";
|
||||
public static final String K_SESSION_WATERMARK = "sessionWatermark";
|
||||
public static final String SUB_VOTE = "Vote";
|
||||
public static final String SUB_VOTE_ONLINE = "VoteOnline";
|
||||
public static final String SUB_VOTE_UPDATE = "VoteUpdate";
|
||||
/** Internal queue metadata; never a backend delivery target. */
|
||||
@@ -121,6 +170,8 @@ public final class VotingPluginWire {
|
||||
public static final String K_NUMBER_OF_VOTES = "numberOfVotes";
|
||||
public static final String K_DELAY_VALIDATED = "delayValidated";
|
||||
public static final String K_QUEUED_DELIVERY = "queuedDelivery";
|
||||
/** Guide-only delivery hint; cross-node occurrence order remains unconfirmed. */
|
||||
public static final String K_SESSION_DELIVERY_FRESH = "sessionDeliveryFresh";
|
||||
public static final String K_VOTE_DELIVERY_ACK_VERSION = "voteDeliveryAckVersion";
|
||||
public static final String K_AUTHENTICATED_SOCKET_VOTE_DELIVERY_VERSION =
|
||||
"authenticatedSocketVoteDeliveryVersion";
|
||||
@@ -186,7 +237,7 @@ public final class VotingPluginWire {
|
||||
.put(K_TIME, time).put(K_WAS_ONLINE, wasOnline).put(K_WAS_ONLINE_KNOWN, true)
|
||||
.put(K_REAL_VOTE, realVote).put(K_TOTALS, safe(totals))
|
||||
.put(K_VOTE_ID, voteId == null ? "" : voteId.toString()).put(K_SET_TOTALS, true)
|
||||
.put(K_MANAGE_TOTALS, manageTotals).put(K_QUEUED_DELIVERY, false)
|
||||
.put(K_MANAGE_TOTALS, manageTotals).put(K_QUEUED_DELIVERY, false).put(K_SESSION_DELIVERY_FRESH, false)
|
||||
.put(K_BUNGEE_BROADCAST, bungeeBroadcast).put(K_NUM, num)
|
||||
.put(K_NUMBER_OF_VOTES, numberOfVotes);
|
||||
}
|
||||
@@ -252,6 +303,19 @@ public final class VotingPluginWire {
|
||||
return builder.put(K_QUEUED_DELIVERY, true).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retains origin provenance for guide observations without changing reward queue
|
||||
* classification. An empty origin explicitly means the original ingress is unknown
|
||||
* (for example a cached delivery); it does not claim a reliable multi-proxy sender.
|
||||
*/
|
||||
public static JsonEnvelope unconfirmedSessionDelivery(JsonEnvelope envelope, String origin) {
|
||||
JsonEnvelope.Builder builder = JsonEnvelope.builder(envelope.getSubChannel()).schema(envelope.getSchema());
|
||||
for (Map.Entry<String, String> field : envelope.getFields().entrySet()) {
|
||||
builder.put(field.getKey(), field.getValue());
|
||||
}
|
||||
return builder.put(K_MULTI_PROXY_ORIGIN, safe(origin)).put(K_SESSION_DELIVERY_FRESH, false).build();
|
||||
}
|
||||
|
||||
/** Removes the additive queue marker for cache rows created by pre-marker proxies. */
|
||||
public static JsonEnvelope legacyUnclassifiedDelivery(JsonEnvelope envelope) {
|
||||
JsonEnvelope.Builder builder = JsonEnvelope.builder(envelope.getSubChannel()).schema(envelope.getSchema());
|
||||
|
||||
+1
-1
@@ -629,7 +629,7 @@ public class VotingPluginBungee extends Plugin implements Listener {
|
||||
+ MinecraftUsernameValidator.sanitizeForLog(player));
|
||||
return;
|
||||
}
|
||||
pending = new PendingIncomingVote(UUID.randomUUID(), player, service, System.currentTimeMillis());
|
||||
pending = new PendingIncomingVote(com.bencodez.votingplugin.core.session.ProxyVoteSequence.nextVoteId(), player, service, System.currentTimeMillis());
|
||||
if (votingPluginProxy == null || !votingPluginProxy.retainIncomingVoteForRestart(pending)) {
|
||||
getLogger().severe("Pending vote admission is full and durable overflow failed; vote was not accepted for "
|
||||
+ MinecraftUsernameValidator.sanitizeForLog(player));
|
||||
|
||||
+16
@@ -958,6 +958,8 @@ public abstract class MultiProxyHandler {
|
||||
&& getRedisPrefix() != null && !getRedisPrefix().isEmpty();
|
||||
}
|
||||
|
||||
protected void onSessionFenceReply(JsonEnvelope message) { }
|
||||
|
||||
private synchronized void loadMultiProxyRedisListener(String channel) {
|
||||
if (!registeredRedisChannels.add(channel)) return;
|
||||
try {
|
||||
@@ -1161,6 +1163,18 @@ public abstract class MultiProxyHandler {
|
||||
f = new HashMap<>();
|
||||
}
|
||||
|
||||
if (sub.equals(VotingPluginWire.SUB_VOTE_SESSION_FENCE_REQUEST)) {
|
||||
var request = VotingPluginWire.readSessionFenceRequest(envelope);
|
||||
if (request != null && configuredMultiProxyRecipientNames().containsKey(request.origin().toLowerCase(Locale.ROOT)))
|
||||
sendMultiProxyEnvelopeAccepted(VotingPluginWire.sessionFenceReply(request,
|
||||
com.bencodez.votingplugin.core.session.ProxyVoteSequence.currentFence()), List.of(request.origin()));
|
||||
return;
|
||||
}
|
||||
if (sub.equals(VotingPluginWire.SUB_VOTE_SESSION_FENCE_REPLY)) {
|
||||
var reply = VotingPluginWire.readSessionFenceReply(envelope);
|
||||
if (reply != null && reply.request().origin().equals(getMultiProxyServerName())) onSessionFenceReply(envelope);
|
||||
return;
|
||||
}
|
||||
if (sub.equalsIgnoreCase(VotingPluginWire.SUB_STATUS)) {
|
||||
logInfo("Multi-proxy status message received");
|
||||
return;
|
||||
@@ -1276,6 +1290,8 @@ public abstract class MultiProxyHandler {
|
||||
if (origin.isBlank()) {
|
||||
triggerVote(player, service, realVote, true, 0L, VoteTotalsSnapshot.parseStorage(totals), uuid);
|
||||
} else {
|
||||
// Preserve the receiver-local time used by ordinary backend cooldowns.
|
||||
// Guide confirmation rejects forwarded ingress independently of this time.
|
||||
triggerVote(player, service, realVote, true, 0L, VoteTotalsSnapshot.parseStorage(totals), uuid,
|
||||
wireVote.voteId, origin, wireVote.delayValidated, wireVote.delayValidationKnown);
|
||||
}
|
||||
|
||||
+1
-1
@@ -524,7 +524,7 @@ public class VotingPluginVelocity {
|
||||
MinecraftUsernameValidator.sanitizeForLog(player));
|
||||
return;
|
||||
}
|
||||
pending = new PendingIncomingVote(UUID.randomUUID(), player, service, System.currentTimeMillis());
|
||||
pending = new PendingIncomingVote(com.bencodez.votingplugin.core.session.ProxyVoteSequence.nextVoteId(), player, service, System.currentTimeMillis());
|
||||
if (votingPluginProxy == null || !votingPluginProxy.retainIncomingVoteForRestart(pending)) {
|
||||
logger.error("Pending vote admission is full and durable overflow failed; vote was not accepted for {}",
|
||||
MinecraftUsernameValidator.sanitizeForLog(player));
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package com.bencodez.votingplugin.session;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
import io.github.projectunified.unidialog.core.payload.DialogPayload;
|
||||
|
||||
/** Lazy host adapter over already bundled UniDialog; owns its own callbacks and event hook. */
|
||||
final class GuidedSessionDialogs implements SessionDialogView {
|
||||
private record Invocation(BiConsumer<UUID, String> callback, UUID owner, String action) { }
|
||||
private final ThreadLocal<List<Invocation>> pendingCallbacks = new ThreadLocal<>();
|
||||
private static final String NAMESPACE = "votingplugin_session";
|
||||
private record Platform(String manager, Class<?> event, boolean paper) {}
|
||||
private final Object manager;
|
||||
private final Platform platform;
|
||||
|
||||
private static Platform probe() throws ReflectiveOperationException {
|
||||
ClassLoader loader = GuidedSessionDialogs.class.getClassLoader();
|
||||
try {
|
||||
Class<?> dialog = Class.forName("io.papermc.paper.dialog.Dialog", false, loader);
|
||||
Class<?> event = Class.forName("io.papermc.paper.event.player.PlayerCustomClickEvent", false, loader);
|
||||
event.getMethod("getIdentifier");
|
||||
event.getMethod("getCommonConnection");
|
||||
Class<?> dialogLike = Class.forName("net.kyori.adventure.dialog.DialogLike", false, loader);
|
||||
Player.class.getMethod("showDialog", dialogLike);
|
||||
Class.forName("io.papermc.paper.registry.data.dialog.ActionButton", false, loader);
|
||||
if (!dialogLike.isAssignableFrom(dialog)) throw new NoSuchMethodException("Incompatible Paper dialog API");
|
||||
return new Platform("io.github.projectunified.unidialog.paper.PaperDialogManager", event, true);
|
||||
} catch (ClassNotFoundException | NoSuchMethodException noPaper) {
|
||||
Class<?> dialog = Class.forName("net.md_5.bungee.api.dialog.Dialog", false, loader);
|
||||
Class<?> event = Class.forName("org.bukkit.event.player.PlayerCustomClickEvent", false, loader);
|
||||
event.getMethod("getId");
|
||||
event.getMethod("getPlayer");
|
||||
Player.class.getMethod("showDialog", dialog);
|
||||
return new Platform("io.github.projectunified.unidialog.spigot.SpigotDialogManager", event, false);
|
||||
}
|
||||
}
|
||||
static boolean supported() {
|
||||
try { probe(); return true; }
|
||||
catch (ReflectiveOperationException | LinkageError unavailable) { return false; }
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
GuidedSessionDialogs(VotingPluginMain plugin) {
|
||||
try {
|
||||
platform = probe();
|
||||
Class<?> type = Class.forName(platform.manager(), true, getClass().getClassLoader());
|
||||
manager = type.getConstructor(Plugin.class, String.class).newInstance(plugin, NAMESPACE);
|
||||
Method dispatch = type.getMethod("onCustomClick", platform.event());
|
||||
type.getMethod("registerCustomAction", String.class, String.class, Consumer.class);
|
||||
type.getMethod("unregisterCustomAction", String.class, String.class);
|
||||
// Do not register the library listener: all its HashMap accesses use this private lock.
|
||||
plugin.getServer().getPluginManager().registerEvent((Class<? extends Event>) platform.event(),
|
||||
new Listener() {}, EventPriority.NORMAL, (listener, event) -> {
|
||||
if (!platform.event().isInstance(event)) return;
|
||||
List<Invocation> callbacks = new ArrayList<>();
|
||||
pendingCallbacks.set(callbacks);
|
||||
try {
|
||||
synchronized (manager) { dispatch.invoke(manager, event); }
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
plugin.getLogger().warning("Voting session dialog callback failed: " + failure.getClass().getSimpleName());
|
||||
} finally {
|
||||
pendingCallbacks.remove();
|
||||
}
|
||||
// Controller callbacks can acquire their own locks or schedule work.
|
||||
// Never invoke them while holding the library manager's registry lock.
|
||||
for (Invocation callback : callbacks)
|
||||
callback.callback().accept(callback.owner(), callback.action());
|
||||
}, plugin, false);
|
||||
} catch (ReflectiveOperationException | LinkageError failure) {
|
||||
throw new IllegalStateException("Native dialog API unavailable", failure);
|
||||
}
|
||||
}
|
||||
public Runnable show(Player player, String title, String body, List<Button> buttons,
|
||||
BiConsumer<UUID, String> action) {
|
||||
List<String> ids = new ArrayList<>();
|
||||
synchronized (manager) {
|
||||
try {
|
||||
Object dialog = call(manager, "createMultiActionDialog", new Class<?>[0]);
|
||||
call(dialog, "title", new Class<?>[] {String.class}, title);
|
||||
call(dialog, "canCloseWithEscape", new Class<?>[] {boolean.class}, true);
|
||||
call(dialog, "pause", new Class<?>[] {boolean.class}, false);
|
||||
call(dialog, "afterAction", new Class<?>[] {io.github.projectunified.unidialog.core.dialog.Dialog.AfterAction.class},
|
||||
io.github.projectunified.unidialog.core.dialog.Dialog.AfterAction.CLOSE);
|
||||
call(dialog, "columns", new Class<?>[] {int.class}, 1);
|
||||
call(dialog, "body", new Class<?>[] {Consumer.class}, (Consumer<Object>) builder -> {
|
||||
Object text = call(builder, "text", new Class<?>[0]);
|
||||
call(text, "text", new Class<?>[] {String.class}, body);
|
||||
});
|
||||
for (var button : buttons) {
|
||||
String id = UUID.randomUUID().toString();
|
||||
if (button.action() != null) {
|
||||
Consumer<DialogPayload> callback = payload -> {
|
||||
List<Invocation> queued = pendingCallbacks.get();
|
||||
if (queued != null && queued.size() < 16)
|
||||
queued.add(new Invocation(action, payload.owner(), button.action()));
|
||||
};
|
||||
call(manager, "registerCustomAction", new Class<?>[] {String.class, String.class, Consumer.class},
|
||||
NAMESPACE, id, callback);
|
||||
ids.add(id);
|
||||
}
|
||||
call(dialog, "action", new Class<?>[] {Consumer.class}, (Consumer<Object>) builder -> {
|
||||
call(builder, "label", new Class<?>[] {String.class}, button.label());
|
||||
call(builder, "tooltip", new Class<?>[] {String.class}, button.tooltip());
|
||||
if (button.url() != null) call(builder, "openUrl", new Class<?>[] {String.class}, button.url());
|
||||
else call(builder, "dynamicCustom", new Class<?>[] {String.class, String.class}, NAMESPACE, id);
|
||||
});
|
||||
}
|
||||
Object opener = call(dialog, "opener", new Class<?>[0]);
|
||||
Class<?> audience = platform.paper()
|
||||
? Class.forName("net.kyori.adventure.audience.Audience", false, getClass().getClassLoader()) : Player.class;
|
||||
call(opener, "open", new Class<?>[] {audience}, player);
|
||||
} catch (RuntimeException | LinkageError | ClassNotFoundException failure) {
|
||||
release(ids);
|
||||
throw new IllegalStateException("Native dialog could not open", failure);
|
||||
}
|
||||
}
|
||||
List<String> owned = List.copyOf(ids);
|
||||
return () -> { synchronized (manager) { release(owned); } };
|
||||
}
|
||||
private void release(List<String> ids) {
|
||||
for (String id : ids) call(manager, "unregisterCustomAction", new Class<?>[] {String.class, String.class}, NAMESPACE, id);
|
||||
}
|
||||
private static Object call(Object target, String name, Class<?>[] types, Object... arguments) {
|
||||
try { return target.getClass().getMethod(name, types).invoke(target, arguments); }
|
||||
catch (ReflectiveOperationException failure) { throw new IllegalStateException("Native dialog method unavailable: " + name, failure); }
|
||||
}
|
||||
}
|
||||
+429
@@ -0,0 +1,429 @@
|
||||
package com.bencodez.votingplugin.session;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
import com.bencodez.votingplugin.core.session.GuidedVoteSession;
|
||||
import com.bencodez.votingplugin.events.PlayerPostVoteEvent;
|
||||
import com.bencodez.votingplugin.util.BukkitCompletionScheduler;
|
||||
import com.bencodez.votingplugin.votesites.VoteSite;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
|
||||
/** Observation-only native guidance with a clear chat fallback on older hosts. */
|
||||
public final class GuidedVotingSessions implements Listener {
|
||||
private final VotingPluginMain plugin;
|
||||
private final Map<UUID, GuidedVoteSession> sessions = new LinkedHashMap<>();
|
||||
private final Map<UUID, Long> requests = new LinkedHashMap<>();
|
||||
private record EarlyReceipt(UUID storageId, String site, UUID occurrence, long time, boolean observed, boolean proxy) { }
|
||||
private final Map<UUID, UUID> storageIds = new LinkedHashMap<>();
|
||||
private final Map<UUID, String> playerNames = new LinkedHashMap<>();
|
||||
private final Map<UUID, Player> owners = new LinkedHashMap<>();
|
||||
private final Map<UUID, Map<String, List<EarlyReceipt>>> early = new LinkedHashMap<>();
|
||||
private record ProxyRequest(UUID nonce, String server, long startedNanos, boolean sent) { }
|
||||
private final Map<UUID, ProxyRequest> proxyRequests = new LinkedHashMap<>();
|
||||
private long lifecycle;
|
||||
private final java.util.Set<Long> outstanding = new java.util.HashSet<>();
|
||||
private final Map<UUID, Long> pending = new LinkedHashMap<>();
|
||||
private long generation;
|
||||
private final java.util.function.Supplier<SessionDialogView> dialogFactory;
|
||||
private SessionDialogView dialogView;
|
||||
private boolean dialogChecked;
|
||||
private record DialogRegistration(UUID nonce, Runnable cleanup) { }
|
||||
private java.util.concurrent.ScheduledFuture<?> dialogCleanup;
|
||||
private final Map<UUID, DialogRegistration> dialogs = new LinkedHashMap<>();
|
||||
private final java.util.Set<UUID> automaticRefresh = new java.util.HashSet<>();
|
||||
|
||||
public GuidedVotingSessions(VotingPluginMain plugin) {
|
||||
this(plugin, () -> GuidedSessionDialogs.supported() ? new GuidedSessionDialogs(plugin) : null);
|
||||
}
|
||||
GuidedVotingSessions(VotingPluginMain plugin, java.util.function.Supplier<SessionDialogView> factory) {
|
||||
this.plugin = plugin; this.dialogFactory = factory;
|
||||
}
|
||||
public synchronized void clear() { for (UUID owner : List.copyOf(dialogs.keySet())) retireDialog(owner); if (dialogCleanup != null) { dialogCleanup.cancel(false); dialogCleanup = null; } automaticRefresh.clear(); sessions.clear(); proxyRequests.clear(); requests.clear(); storageIds.clear(); playerNames.clear(); owners.clear(); early.clear(); pending.clear(); generation++; lifecycle++; }
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public synchronized void credited(PlayerPostVoteEvent event) {
|
||||
if (event.getVoteSite() == null || !event.isRealVote() || event.isCancelled()
|
||||
|| event.getVoteUUID() == null || event.getUuid() == null) return;
|
||||
boolean proxy = event.isProxySessionDelivery() || event.isBungee();
|
||||
// Proxy transport flags and clocks prove no original order; negotiated source fences do.
|
||||
if (proxy ? !event.isProxySessionRealVote() : event.isUnconfirmedLocalSessionDelivery()) return;
|
||||
boolean observed = event.isLiveLocalSessionDelivery() && event.getBackendObservationOrder() != 0;
|
||||
long time = observed ? event.getBackendObservationOrder() : event.getVoteTime();
|
||||
expire(System.currentTimeMillis());
|
||||
String key = event.getVoteSite().getKey();
|
||||
for (var entry : sessions.entrySet()) {
|
||||
UUID owner = entry.getKey(); GuidedVoteSession session = entry.getValue();
|
||||
if (event.getUuid().equals(storageIds.get(owner))) {
|
||||
var before = session.view("");
|
||||
if (proxy) session.acceptedProxy(key, event.getVoteUUID());
|
||||
else if (observed) session.acceptedObserved(key, event.getVoteUUID(), time);
|
||||
else session.accepted(key, event.getVoteUUID(), time, true, false);
|
||||
if (confirmedTarget(before, session.view(""))) refresh(owner, session);
|
||||
} else if (!storageIds.containsKey(owner) && session.candidate(key)
|
||||
&& (proxy || observed || time > session.started()) && event.getPlayerName() != null
|
||||
&& event.getPlayerName().equalsIgnoreCase(playerNames.get(owner))) {
|
||||
// Name only routes a bounded provisional receipt. Resolved storage UUID must
|
||||
// match before it can confirm progress; no player/entity access occurs here.
|
||||
// A failed lookup releases pending work, but this active guide still owns
|
||||
// its bounded provisional receipts until identity resolution or expiration.
|
||||
var receipts = early.computeIfAbsent(owner, ignored -> new LinkedHashMap<>())
|
||||
.computeIfAbsent(key, ignored -> new ArrayList<>());
|
||||
receipts.removeIf(receipt -> receipt.occurrence().equals(event.getVoteUUID()));
|
||||
if (receipts.size() >= 4) receipts.remove(0);
|
||||
receipts.add(new EarlyReceipt(event.getUuid(), key, event.getVoteUUID(), time, observed, proxy));
|
||||
}
|
||||
}
|
||||
// Never execute rewards from a notification; a newly accepted occurrence may refresh the guide.
|
||||
}
|
||||
|
||||
/** Recheck current eligibility on the existing worker before reopening a credited guide. */
|
||||
private void refresh(UUID owner, GuidedVoteSession session) {
|
||||
if (session.view("").finished()) return;
|
||||
automaticRefresh.add(owner);
|
||||
Player player = owners.get(owner);
|
||||
if (player == null) return;
|
||||
BukkitCompletionScheduler.run(plugin, player, () -> {
|
||||
synchronized (this) {
|
||||
expire(System.currentTimeMillis());
|
||||
if (sessions.get(owner) != session || owners.get(owner) != player || session.view("").finished()) return;
|
||||
// A current worker owns a pending refresh; its final owner callback
|
||||
// consumes the flag and samples the latest confirmed progress.
|
||||
if (!pending.containsKey(owner)) prepare(player, "automatic");
|
||||
}
|
||||
}, () -> { }, () -> { });
|
||||
}
|
||||
|
||||
/** Transport callback: bounded identities only; no Bukkit, user storage or reward execution. */
|
||||
public synchronized void proxyFence(com.bencodez.simpleapi.servercomm.codec.JsonEnvelope message) {
|
||||
var reply = com.bencodez.votingplugin.proxy.VotingPluginWire.readSessionFenceReply(message);
|
||||
if (reply == null) return;
|
||||
expire(System.currentTimeMillis());
|
||||
UUID owner = reply.request().player();
|
||||
ProxyRequest request = proxyRequests.get(owner);
|
||||
GuidedVoteSession session = sessions.get(owner);
|
||||
if (request == null || !request.sent() || session == null || !request.nonce().equals(reply.request().nonce())
|
||||
|| !request.server().equals(reply.request().server())
|
||||
|| System.nanoTime() - request.startedNanos() > java.util.concurrent.TimeUnit.SECONDS.toNanos(30)) return;
|
||||
var before = session.view("");
|
||||
session.bindProxyFence(reply.fence());
|
||||
if (confirmedTarget(before, session.view(""))) refresh(owner, session);
|
||||
}
|
||||
|
||||
private static boolean confirmedTarget(GuidedVoteSession.View before, GuidedVoteSession.View after) {
|
||||
return before.current() != null && after.current() != null
|
||||
&& before.current().site().key().equals(after.current().site().key())
|
||||
&& before.current().status() != GuidedVoteSession.Status.RECEIVED
|
||||
&& after.current().status() == GuidedVoteSession.Status.RECEIVED;
|
||||
}
|
||||
|
||||
/** Capture a registration's lifecycle before the command framework queues asynchronous execution. */
|
||||
public synchronized java.util.function.Consumer<Player> commandHandler(String action) {
|
||||
long epoch = lifecycle;
|
||||
return player -> BukkitCompletionScheduler.run(plugin, player, () -> {
|
||||
synchronized (this) { if (epoch == lifecycle) prepare(player, action); }
|
||||
}, () -> { }, () -> { });
|
||||
}
|
||||
|
||||
public void command(Player player, String action) {
|
||||
commandHandler(action).accept(player);
|
||||
}
|
||||
private void prepare(Player player, String action) {
|
||||
if (!player.isOnline() || plugin.getServer().getPlayer(player.getUniqueId()) != player) return;
|
||||
if (!plugin.getConfigFile().getData().getBoolean("GuidedVotingSession.Enabled", false)) {
|
||||
player.sendMessage("Guided voting sessions are disabled by this server."); return;
|
||||
}
|
||||
if (!plugin.getCommandLoader().canUseGuidedSession(player)) return;
|
||||
UUID uuid = player.getUniqueId();
|
||||
UUID storageUuid = VotingPluginMain.placeholderStorageUuid(player, plugin.getOptions().isOnlineMode());
|
||||
String name = player.getName();
|
||||
List<VoteSite> visible = new ArrayList<>();
|
||||
for (VoteSite site : plugin.getVoteSiteManager().getVoteSites()) {
|
||||
if (site.isEnabled() && !site.isHidden() && (site.getPermissionToView().isEmpty()
|
||||
|| player.hasPermission(site.getPermissionToView()))) visible.add(site);
|
||||
if (visible.size() >= 100) break;
|
||||
}
|
||||
GuidedVoteSession session;
|
||||
boolean creating;
|
||||
long request;
|
||||
com.bencodez.votingplugin.proxy.VotingPluginWire.SessionFenceRequest proxyRequest;
|
||||
synchronized (this) {
|
||||
long now = System.currentTimeMillis();
|
||||
expire(now);
|
||||
if (action.equals("automatic") && !sessions.containsKey(uuid)) return;
|
||||
if (pending.containsKey(uuid) || outstanding.size() >= 64) {
|
||||
player.sendMessage("Voting status is already being checked or busy. Please try again shortly."); return;
|
||||
}
|
||||
if (!sessions.containsKey(uuid) && sessions.size() >= 2048) {
|
||||
player.sendMessage("Voting sessions are busy. Please try again later."); return;
|
||||
}
|
||||
if (action.equals("restart")) { retireDialog(uuid); automaticRefresh.remove(uuid); sessions.remove(uuid); storageIds.remove(uuid); early.remove(uuid); proxyRequests.remove(uuid); }
|
||||
creating = !sessions.containsKey(uuid);
|
||||
session = sessions.computeIfAbsent(uuid, ignored -> new GuidedVoteSession(now));
|
||||
proxyRequest = null;
|
||||
if (creating && plugin.getBungeeSettings().isUseBungeecoord()) {
|
||||
var fenceRequest = new ProxyRequest(UUID.randomUUID(), plugin.getOptions().getServer(), 0, false);
|
||||
proxyRequests.put(uuid, fenceRequest);
|
||||
proxyRequest = new com.bencodez.votingplugin.proxy.VotingPluginWire.SessionFenceRequest(
|
||||
uuid, fenceRequest.nonce(), fenceRequest.server(), "");
|
||||
}
|
||||
session.bindCandidates(visible.stream().map(site -> new GuidedVoteSession.Site(site.getKey(),
|
||||
site.getDisplayName(), site.getVoteURL(true), false, 0)).toList());
|
||||
playerNames.put(uuid, name);
|
||||
owners.put(uuid, player);
|
||||
request = ++generation;
|
||||
requests.put(uuid, request);
|
||||
pending.put(uuid, request);
|
||||
outstanding.add(request);
|
||||
}
|
||||
final var requestToSend = proxyRequest;
|
||||
try {
|
||||
plugin.getTimer().execute(() -> {
|
||||
try {
|
||||
synchronized (this) { if (!current(uuid, session, request)) return; }
|
||||
if (requestToSend != null) {
|
||||
var handler = plugin.getBackendProxyHandler();
|
||||
if (handler != null && handler.getGlobalMessageHandler() != null) {
|
||||
synchronized (this) {
|
||||
if (!current(uuid, session, request)) return;
|
||||
var queued = proxyRequests.get(uuid);
|
||||
if (queued == null || !queued.nonce().equals(requestToSend.nonce())) return;
|
||||
// Queue wait is not reply time. Publish before sending so an
|
||||
// immediate transport reply can bind this exact live request.
|
||||
proxyRequests.put(uuid, new ProxyRequest(queued.nonce(), queued.server(), System.nanoTime(), true));
|
||||
}
|
||||
handler.getGlobalMessageHandler().sendMessage(
|
||||
com.bencodez.votingplugin.proxy.VotingPluginWire.sessionFenceRequest(requestToSend));
|
||||
}
|
||||
}
|
||||
var currentSites = new java.util.HashMap<String, VoteSite>();
|
||||
for (VoteSite site : plugin.getVoteSiteManager().getVoteSites()) currentSites.put(site.getKey(), site);
|
||||
var sampledSites = new java.util.HashMap<String, VoteSite>();
|
||||
var user = plugin.getVotingPluginUserManager().getVotingPluginUser(storageUuid, name);
|
||||
String storedName = user.getPlayerName();
|
||||
String voteName = storedName == null ? name : storedName;
|
||||
var lastVotes = new java.util.HashMap<String, Long>();
|
||||
user.getLastVotes().forEach((site, time) -> lastVotes.put(site.getKey(), time));
|
||||
List<GuidedVoteSession.Site> snapshots = new ArrayList<>();
|
||||
for (VoteSite original : visible) {
|
||||
VoteSite site = currentSites.get(original.getKey());
|
||||
if (site == null || !site.isEnabled() || site.isHidden()) continue;
|
||||
sampledSites.put(site.getKey(), site);
|
||||
snapshots.add(new GuidedVoteSession.Site(site.getKey(), site.getDisplayName(),
|
||||
com.bencodez.advancedcore.api.messages.PlaceholderUtils.replacePlaceHolder(site.getVoteURL(true),
|
||||
"player", voteName),
|
||||
user.canVoteSite(site, lastVotes.getOrDefault(site.getKey(), 0L)),
|
||||
plugin.getBungeeSettings().isUseBungeecoord() ? 0 : lastVotes.getOrDefault(site.getKey(), 0L)));
|
||||
}
|
||||
synchronized (this) {
|
||||
if (!current(uuid, session, request)) return;
|
||||
UUID storageId = user.getJavaUUID();
|
||||
storageIds.put(uuid, storageId);
|
||||
var provisional = early.remove(uuid);
|
||||
if (provisional != null) for (var receipts : provisional.values()) for (var receipt : receipts) {
|
||||
if (receipt.storageId().equals(storageId)) {
|
||||
if (receipt.proxy()) session.acceptedProxy(receipt.site(), receipt.occurrence());
|
||||
else if (receipt.observed()) session.acceptedObserved(receipt.site(), receipt.occurrence(), receipt.time());
|
||||
else session.accepted(receipt.site(), receipt.occurrence(), receipt.time(), true, false);
|
||||
}
|
||||
}
|
||||
session.refresh(snapshots);
|
||||
// Storage sampling is complete before an owner callback can render.
|
||||
// A newly credited vote must be able to prepare its successor even
|
||||
// when this callback completes before the worker enters finally.
|
||||
pending.remove(uuid, request); outstanding.remove(request);
|
||||
}
|
||||
BukkitCompletionScheduler.run(plugin, player, () -> {
|
||||
synchronized (this) { if (!current(uuid, session, request)) return; }
|
||||
if (player.isOnline() && plugin.getConfigFile().getData().getBoolean("GuidedVotingSession.Enabled", false)
|
||||
&& plugin.getCommandLoader().canUseGuidedSession(player)) {
|
||||
synchronized (this) {
|
||||
if (!current(uuid, session, request)) return;
|
||||
boolean automatic = automaticRefresh.remove(uuid) || action.equals("automatic");
|
||||
if (action.equals("automatic") && session.view("").finished()) return;
|
||||
// Visibility may change after the worker's storage sample.
|
||||
// Refresh from owner-validated immutable snapshots before
|
||||
// choosing the next target; no storage is accessed here.
|
||||
var ownerVisible = visibleView(player, session.view(""), sampledSites);
|
||||
session.refresh(ownerVisible.entries().stream().map(GuidedVoteSession.Entry::site).toList());
|
||||
if (automatic || action.isEmpty()) session.selectNextAwaiting();
|
||||
var view = session.view(action);
|
||||
render(player, visibleView(player, view, sampledSites));
|
||||
}
|
||||
}
|
||||
}, () -> { }, () -> { });
|
||||
} catch (RuntimeException failure) {
|
||||
plugin.debug(failure);
|
||||
BukkitCompletionScheduler.run(plugin, player, () -> {
|
||||
synchronized (this) { if (!current(uuid, session, request)) return; }
|
||||
if (player.isOnline() && plugin.getConfigFile().getData().getBoolean("GuidedVotingSession.Enabled", false)
|
||||
&& plugin.getCommandLoader().canUseGuidedSession(player)) player.sendMessage("Could not check voting status. Please try " + sessionCommand("check") + ".");
|
||||
}, () -> { }, () -> { });
|
||||
} finally {
|
||||
synchronized (this) { pending.remove(uuid, request); outstanding.remove(request); }
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException unavailable) {
|
||||
synchronized (this) {
|
||||
pending.remove(uuid, request); outstanding.remove(request);
|
||||
// No worker ran to send the initial fence. Retire only this new guide,
|
||||
// so an ordinary reopen retries creation without resetting existing progress.
|
||||
if (creating && sessions.get(uuid) == session && Long.valueOf(request).equals(requests.get(uuid))) {
|
||||
sessions.remove(uuid); requests.remove(uuid); storageIds.remove(uuid);
|
||||
playerNames.remove(uuid); early.remove(uuid); proxyRequests.remove(uuid);
|
||||
}
|
||||
}
|
||||
player.sendMessage("Voting status is temporarily unavailable. Please try again.");
|
||||
}
|
||||
}
|
||||
/** Expiration releases owner admission, while outstanding work retains its global slot until it finishes. */
|
||||
private synchronized void expire(long now) {
|
||||
int minutes = Math.max(1, Math.min(120, plugin.getConfigFile().getData().getInt("GuidedVotingSession.TimeoutMinutes", 30)));
|
||||
sessions.entrySet().removeIf(e -> {
|
||||
if (now - e.getValue().started() < minutes * 60_000L) return false;
|
||||
retireDialog(e.getKey()); automaticRefresh.remove(e.getKey()); return true;
|
||||
});
|
||||
requests.keySet().retainAll(sessions.keySet());
|
||||
pending.keySet().retainAll(sessions.keySet());
|
||||
storageIds.keySet().retainAll(sessions.keySet());
|
||||
playerNames.keySet().retainAll(sessions.keySet());
|
||||
owners.keySet().retainAll(sessions.keySet());
|
||||
early.keySet().retainAll(sessions.keySet());
|
||||
proxyRequests.keySet().retainAll(sessions.keySet());
|
||||
}
|
||||
private synchronized boolean current(UUID uuid, GuidedVoteSession session, long request) {
|
||||
expire(System.currentTimeMillis());
|
||||
return sessions.get(uuid) == session && Long.valueOf(request).equals(requests.get(uuid));
|
||||
}
|
||||
/** Recheck visibility on the player context, after any asynchronous wait. */
|
||||
private GuidedVoteSession.View visibleView(Player player, GuidedVoteSession.View view, Map<String, VoteSite> sampledSites) {
|
||||
java.util.Set<String> allowed = new java.util.HashSet<>();
|
||||
for (VoteSite site : plugin.getVoteSiteManager().getVoteSites()) {
|
||||
if (sampledSites.get(site.getKey()) == site && site.isEnabled() && !site.isHidden() && (site.getPermissionToView().isEmpty()
|
||||
|| player.hasPermission(site.getPermissionToView()))) allowed.add(site.getKey());
|
||||
}
|
||||
List<GuidedVoteSession.Entry> entries = new ArrayList<>();
|
||||
for (var entry : view.entries()) entries.add(allowed.contains(entry.site().key()) ? entry
|
||||
: new GuidedVoteSession.Entry(new GuidedVoteSession.Site(entry.site().key(), "Unavailable site",
|
||||
null, false, 0), GuidedVoteSession.Status.UNAVAILABLE));
|
||||
return new GuidedVoteSession.View(List.copyOf(entries), view.index(), view.finished());
|
||||
}
|
||||
private void render(Player player, GuidedVoteSession.View view) {
|
||||
retireDialog(player.getUniqueId());
|
||||
try {
|
||||
if (!dialogChecked) { dialogChecked = true; dialogView = dialogFactory.get(); }
|
||||
if (dialogView != null) { renderDialog(player, view); return; }
|
||||
} catch (RuntimeException | LinkageError unsupported) {
|
||||
plugin.debug(unsupported);
|
||||
}
|
||||
player.sendMessage("Native voting dialogs are unavailable on this server. Showing chat guidance instead.");
|
||||
renderChat(player, view);
|
||||
}
|
||||
private void renderDialog(Player player, GuidedVoteSession.View view) {
|
||||
UUID owner = player.getUniqueId();
|
||||
UUID nonce = UUID.randomUUID();
|
||||
GuidedVoteSession session = sessions.get(owner);
|
||||
List<SessionDialogView.Button> buttons = new ArrayList<>();
|
||||
String body = view.received() + "/" + view.entries().size() + " votes received.";
|
||||
if (view.entries().isEmpty()) body += "\nNo eligible, visible voting sites. Restart after your cooldowns end.";
|
||||
else if (view.finished() || view.complete()) body += view.complete()
|
||||
? "\nAll session votes were received. Thank you!" : "\nSession finished. Unconfirmed votes remain unconfirmed.";
|
||||
else {
|
||||
var entry = view.current();
|
||||
body += "\nSite " + (view.index() + 1) + "/" + view.entries().size() + ": " + entry.site().name()
|
||||
+ "\n" + entry.status() + "\nWaiting for VotingPlugin to receive your vote.";
|
||||
String url = GuidedVoteSession.httpUrl(entry.site().url());
|
||||
if (url != null && (entry.status() == GuidedVoteSession.Status.AWAITING || entry.status() == GuidedVoteSession.Status.SKIPPED))
|
||||
buttons.add(new SessionDialogView.Button("Open voting link", "Your client controls opening the browser.", null, url));
|
||||
else if (url == null) body += "\nThis site has no valid HTTP/HTTPS link. Ask an administrator.";
|
||||
}
|
||||
if (session != null && plugin.getBungeeSettings().isUseBungeecoord() && !session.hasProxyFence())
|
||||
body += "\nProxy confirmation is pending or unavailable.";
|
||||
body += "\nOpening a link never grants a vote or reward.";
|
||||
if (!view.entries().isEmpty() && !view.finished() && !view.complete())
|
||||
for (String action : List.of("previous", "next", "skip", "check", "finish"))
|
||||
buttons.add(new SessionDialogView.Button(action, "", action, null));
|
||||
buttons.add(new SessionDialogView.Button("Restart session", "", "restart", null));
|
||||
buttons.add(new SessionDialogView.Button("Close", "Your current session is retained.", "close", null));
|
||||
java.util.concurrent.atomic.AtomicBoolean used = new java.util.concurrent.atomic.AtomicBoolean();
|
||||
Runnable cleanup = dialogView.show(player, "Voting session", body, List.copyOf(buttons), (actor, action) -> {
|
||||
if (!owner.equals(actor)) return;
|
||||
BukkitCompletionScheduler.run(plugin, player, () -> {
|
||||
synchronized (this) {
|
||||
expire(System.currentTimeMillis());
|
||||
DialogRegistration active = dialogs.get(owner);
|
||||
if (active == null || !nonce.equals(active.nonce()) || sessions.get(owner) != session
|
||||
|| !player.isOnline() || !used.compareAndSet(false, true)) return;
|
||||
retireDialog(owner);
|
||||
if (plugin.getCommandLoader().canUseGuidedSession(player) && !action.equals("close")) prepare(player, action);
|
||||
}
|
||||
}, () -> { }, () -> { });
|
||||
});
|
||||
try {
|
||||
// One bounded sweep owns all callback deadlines. Replacing a view
|
||||
// does not leave cancelled per-view futures in a shared timer queue.
|
||||
if (dialogCleanup == null) dialogCleanup = plugin.getTimer().scheduleWithFixedDelay(() -> {
|
||||
synchronized (this) { expire(System.currentTimeMillis()); }
|
||||
}, 30, 30, java.util.concurrent.TimeUnit.SECONDS);
|
||||
dialogs.put(owner, new DialogRegistration(nonce, cleanup));
|
||||
} catch (RuntimeException failure) {
|
||||
cleanup.run(); throw failure;
|
||||
}
|
||||
}
|
||||
private void retireDialog(UUID owner) {
|
||||
DialogRegistration registration = dialogs.remove(owner);
|
||||
if (registration == null) return;
|
||||
try { registration.cleanup().run(); }
|
||||
catch (RuntimeException failure) { plugin.debug(failure); }
|
||||
}
|
||||
@EventHandler
|
||||
public synchronized void disconnected(org.bukkit.event.player.PlayerQuitEvent event) {
|
||||
UUID owner = event.getPlayer().getUniqueId();
|
||||
retireDialog(owner); requests.remove(owner); pending.remove(owner); automaticRefresh.remove(owner); owners.remove(owner);
|
||||
}
|
||||
private void renderChat(Player player, GuidedVoteSession.View view) {
|
||||
if (plugin.getBungeeSettings().isUseBungeecoord()) {
|
||||
GuidedVoteSession session = sessions.get(player.getUniqueId());
|
||||
if (session != null && !session.hasProxyFence()) player.sendMessage(
|
||||
"Proxy confirmation is pending or unavailable. Only votes after a supporting proxy acknowledges this session can be confirmed.");
|
||||
}
|
||||
player.sendMessage("Voting session: " + view.received() + "/" + view.entries().size() + " votes received.");
|
||||
if (view.entries().isEmpty()) {
|
||||
player.sendMessage("No eligible, visible voting sites. Try " + sessionCommand("restart") + " after your cooldowns end."); return;
|
||||
}
|
||||
if (view.finished() || view.complete()) {
|
||||
player.sendMessage(view.complete() ? "All session votes were received. Thank you!"
|
||||
: "Session finished. Unconfirmed votes remain unconfirmed; normal vote rewards are unchanged.");
|
||||
}
|
||||
for (int i = 0; i < view.entries().size(); i++) {
|
||||
var entry = view.entries().get(i);
|
||||
player.sendMessage((i == view.index() ? "> " : " ") + entry.site().name() + " — " + entry.status());
|
||||
}
|
||||
var entry = view.current();
|
||||
if (!view.finished() && (entry.status() == GuidedVoteSession.Status.AWAITING || entry.status() == GuidedVoteSession.Status.SKIPPED)) {
|
||||
String url = GuidedVoteSession.httpUrl(entry.site().url());
|
||||
if (url != null) button(player, "Open voting link: " + entry.site().name(), ClickEvent.Action.OPEN_URL, url);
|
||||
else player.sendMessage("This site has no valid HTTP/HTTPS link. Ask an administrator.");
|
||||
}
|
||||
for (String action : List.of("previous", "next", "skip", "check", "finish", "restart"))
|
||||
button(player, "[" + action + "]", ClickEvent.Action.RUN_COMMAND, sessionCommand(action));
|
||||
player.sendMessage("A link or click is not vote confirmation. Check again after the vote arrives.");
|
||||
}
|
||||
private String sessionCommand(String action) {
|
||||
return "/" + plugin.getCommandLoader().getVoteCommandName() + " session " + action;
|
||||
}
|
||||
private static void button(Player player, String title, ClickEvent.Action action, String value) {
|
||||
TextComponent component = new TextComponent(title);
|
||||
component.setClickEvent(new ClickEvent(action, value));
|
||||
player.spigot().sendMessage(component);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.bencodez.votingplugin.session;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/** A player-owned native view; cleanup releases only this view's action handlers. */
|
||||
interface SessionDialogView {
|
||||
record Button(String label, String tooltip, String action, String url) { }
|
||||
Runnable show(Player player, String title, String body, List<Button> buttons,
|
||||
BiConsumer<UUID, String> action);
|
||||
}
|
||||
+1
-1
@@ -120,7 +120,7 @@ public final class VotePartyRewardHandler {
|
||||
return triggeringUser;
|
||||
}
|
||||
VotingPluginUser user = onlinePlayer == null
|
||||
? plugin.getVotingPluginUserManager().getVotingPluginUser(uuid)
|
||||
? plugin.getVotingPluginUserManager().getVotingPluginUser(uuid, true)
|
||||
: plugin.getVotingPluginUserManager().getVotingPluginUser(onlinePlayer);
|
||||
user.userDataFetechMode(UserDataFetchMode.NO_CACHE);
|
||||
return user;
|
||||
|
||||
+21
@@ -40,6 +40,7 @@ public class VoteStreakHandler {
|
||||
private final Map<String, VoteStreakDefinition> byId = new LinkedHashMap<>();
|
||||
private final Map<String, VoteStreakDefinition> byProgressGroup = new LinkedHashMap<>();
|
||||
private final List<VoteStreakDefinition> ordered = new ArrayList<>();
|
||||
private volatile List<VoteStreakDefinition> displayDefinitions = List.of();
|
||||
|
||||
public VoteStreakHandler(VotingPluginMain plugin) {
|
||||
this.plugin = plugin;
|
||||
@@ -56,6 +57,26 @@ public class VoteStreakHandler {
|
||||
// VoteStreak definitions live in specialrewards.yml now (Option B)
|
||||
ConfigurationSection root = plugin.getSpecialRewardsConfig().getData();
|
||||
new VoteStreakConfigLoader().load(root);
|
||||
displayDefinitions = ordered.stream().filter(VoteStreakDefinition::isEnabled).toList();
|
||||
}
|
||||
|
||||
/** Worker-only, observational view. Does not advance periods, save state or execute rewards. */
|
||||
public com.bencodez.votingplugin.streaks.VoteStreaksSnapshot snapshotForDisplay(VotingPluginUser user) {
|
||||
var definitions = displayDefinitions;
|
||||
var periods = List.of(
|
||||
new com.bencodez.votingplugin.streaks.VoteStreaksSnapshot.Period(VoteStreakType.DAILY, user.getDayVoteStreak(), user.getBestDayVoteStreak()),
|
||||
new com.bencodez.votingplugin.streaks.VoteStreaksSnapshot.Period(VoteStreakType.WEEKLY, user.getWeekVoteStreak(), user.getBestWeekVoteStreak()),
|
||||
new com.bencodez.votingplugin.streaks.VoteStreaksSnapshot.Period(VoteStreakType.MONTHLY, user.getMonthVoteStreak(), user.getBestMonthVoteStreak()));
|
||||
var states = new LinkedHashMap<String, StreakState>();
|
||||
var rows = new ArrayList<com.bencodez.votingplugin.streaks.VoteStreaksSnapshot.Milestone>();
|
||||
for (var definition : definitions) {
|
||||
if (rows.size() >= com.bencodez.votingplugin.streaks.VoteStreaksSnapshot.MAX_MILESTONES) break;
|
||||
var state = states.computeIfAbsent(getColumnName(definition), ignored -> readState(user, definition));
|
||||
String label = definition.getId().codePoints().limit(80).collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
|
||||
rows.add(new com.bencodez.votingplugin.streaks.VoteStreaksSnapshot.Milestone(label, definition.getType(), state.streakCount,
|
||||
state.bestStreakCount, definition.getRequiredAmount(), definition.getVotesRequired(), definition.isRecurring()));
|
||||
}
|
||||
return new com.bencodez.votingplugin.streaks.VoteStreaksSnapshot(periods, rows, definitions.size() > rows.size());
|
||||
}
|
||||
|
||||
public List<VoteStreakDefinition> getDefinitions() {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.bencodez.votingplugin.streaks;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import com.bencodez.advancedcore.api.command.CommandHandler;
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
|
||||
public final class VoteStreaksCommand {
|
||||
private VoteStreaksCommand() { }
|
||||
public static void register(VotingPluginMain plugin) {
|
||||
plugin.getVoteCommand().add(new CommandHandler(plugin, new String[] { "Streaks" },
|
||||
VoteStreaksMenu.PERMISSION + "|VotingPlugin.Player", "View your vote streak progression", false) {
|
||||
@Override public void execute(CommandSender sender, String[] args) {
|
||||
if (sender instanceof Player player && plugin.getVoteStreaksMenu() != null) {
|
||||
plugin.getVoteStreaksMenu().open(player);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package com.bencodez.votingplugin.streaks;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import com.bencodez.simpleapi.messages.MessageAPI;
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
import com.bencodez.votingplugin.util.BukkitCompletionScheduler;
|
||||
|
||||
/** One isolated native inventory view; no registrations in existing production GUI managers. */
|
||||
public final class VoteStreaksMenu implements Listener, AutoCloseable {
|
||||
public static final String PERMISSION = "VotingPlugin.Commands.Vote.Streaks";
|
||||
private static final int MAX_SESSIONS = 128, MAX_LOADS = 16;
|
||||
private final VotingPluginMain plugin;
|
||||
private final Consumer<Runnable> worker;
|
||||
private final BiFunction<UUID, String, VoteStreaksSnapshot> snapshot;
|
||||
private final Map<UUID, Session> sessions = new ConcurrentHashMap<>();
|
||||
private final AtomicInteger loading = new AtomicInteger();
|
||||
private final AtomicInteger reloadDepth = new AtomicInteger();
|
||||
private volatile boolean closed;
|
||||
|
||||
public VoteStreaksMenu(VotingPluginMain plugin) {
|
||||
this(plugin, task -> plugin.getUserManager().getDataManager().getTimer().execute(task), (uuid, name) ->
|
||||
plugin.getVoteStreakHandler().snapshotForDisplay(plugin.getVotingPluginUserManager().getVotingPluginUser(uuid, name)));
|
||||
}
|
||||
VoteStreaksMenu(VotingPluginMain plugin, Consumer<Runnable> worker, BiFunction<UUID, String, VoteStreaksSnapshot> snapshot) {
|
||||
this.plugin = plugin; this.worker = worker; this.snapshot = snapshot;
|
||||
}
|
||||
private static final class Session {
|
||||
final Player player;
|
||||
final UUID owner;
|
||||
final String name;
|
||||
final Inventory previous;
|
||||
final boolean ownsPrevious;
|
||||
final long created = System.nanoTime();
|
||||
volatile Holder view;
|
||||
Session(Player player, VoteStreaksMenu manager) {
|
||||
this.player = player; owner = player.getUniqueId(); name = player.getName();
|
||||
previous = player.getOpenInventory().getTopInventory();
|
||||
Holder holder = manager.holder(previous);
|
||||
ownsPrevious = holder != null && holder.session.owner.equals(owner);
|
||||
}
|
||||
}
|
||||
static final class Holder implements InventoryHolder {
|
||||
final VoteStreaksMenu manager;
|
||||
final Session session;
|
||||
final VoteStreaksSnapshot snapshot;
|
||||
final int page;
|
||||
Inventory inventory;
|
||||
Holder(VoteStreaksMenu manager, Session session, VoteStreaksSnapshot snapshot, int page) {
|
||||
this.manager = manager; this.session = session; this.snapshot = snapshot; this.page = snapshot.clampPage(page);
|
||||
}
|
||||
@Override public Inventory getInventory() { return inventory; }
|
||||
}
|
||||
private static boolean permitted(Player player) {
|
||||
return player.isOnline() && (player.hasPermission(PERMISSION) || player.hasPermission("VotingPlugin.Player"));
|
||||
}
|
||||
private void onPlayer(Player player, Runnable work, Runnable rejected) {
|
||||
BukkitCompletionScheduler.run(plugin, player, work, rejected, rejected);
|
||||
}
|
||||
public void open(Player player) {
|
||||
onPlayer(player, () -> begin(player), () -> { });
|
||||
}
|
||||
private void begin(Player player) {
|
||||
if (closed || reloadDepth.get() != 0 || !permitted(player)) return;
|
||||
if (loading.incrementAndGet() > MAX_LOADS) {
|
||||
loading.decrementAndGet(); player.sendMessage("Vote streaks are busy. Please try again shortly."); return;
|
||||
}
|
||||
Session session;
|
||||
synchronized (sessions) {
|
||||
if (closed || reloadDepth.get() != 0 || (sessions.size() >= MAX_SESSIONS && !sessions.containsKey(player.getUniqueId()))) {
|
||||
loading.decrementAndGet(); player.sendMessage("Vote streaks are unavailable. Please try again shortly."); return;
|
||||
}
|
||||
session = new Session(player, this); sessions.put(session.owner, session);
|
||||
}
|
||||
try {
|
||||
worker.accept(() -> {
|
||||
try {
|
||||
if (!current(session)) return;
|
||||
VoteStreaksSnapshot data = snapshot.apply(session.owner, session.name);
|
||||
onPlayer(player, () -> {
|
||||
if (!current(session)) return;
|
||||
if (!permitted(player) || System.nanoTime() - session.created > 30_000_000_000L
|
||||
|| player.getOpenInventory().getTopInventory() != session.previous) {
|
||||
retire(session); return;
|
||||
}
|
||||
renderSafely(session, data, 0);
|
||||
}, () -> retire(session));
|
||||
} catch (RuntimeException failure) {
|
||||
onPlayer(player, () -> {
|
||||
if (retire(session) && player.isOnline())
|
||||
player.sendMessage("Unable to read vote streaks. Please try again later.");
|
||||
}, () -> retire(session));
|
||||
} finally { loading.decrementAndGet(); }
|
||||
});
|
||||
} catch (RuntimeException rejected) {
|
||||
loading.decrementAndGet(); retire(session);
|
||||
player.sendMessage("Vote streaks are unavailable. Please try again later.");
|
||||
}
|
||||
}
|
||||
private boolean current(Session session) { return !closed && sessions.get(session.owner) == session; }
|
||||
private Holder holder(Inventory inventory) {
|
||||
return inventory != null && inventory.getHolder() instanceof Holder holder && holder.manager == this ? holder : null;
|
||||
}
|
||||
private void renderSafely(Session session, VoteStreaksSnapshot data, int page) {
|
||||
try { render(session, data, page); }
|
||||
catch (RuntimeException failure) {
|
||||
if (sessions.remove(session.owner, session)) {
|
||||
closeView(session);
|
||||
if (session.player.isOnline()) session.player.sendMessage("Unable to open vote streaks. Please try again later.");
|
||||
}
|
||||
}
|
||||
}
|
||||
private void navigate(Holder holder, int page) {
|
||||
onPlayer(holder.session.player, () -> {
|
||||
if (current(holder.session) && holder.session.view == holder
|
||||
&& holder.session.player.getOpenInventory().getTopInventory() == holder.inventory)
|
||||
renderSafely(holder.session, holder.snapshot, page);
|
||||
}, () -> sessions.remove(holder.session.owner, holder.session));
|
||||
}
|
||||
private void render(Session session, VoteStreaksSnapshot data, int page) {
|
||||
if (!current(session) || !permitted(session.player)) return;
|
||||
Holder holder = new Holder(this, session, data, page);
|
||||
String title = plugin.getGui().getChestVoteStreaksName();
|
||||
holder.inventory = Bukkit.createInventory(holder, 54, MessageAPI.colorize(title));
|
||||
for (int i = 0; i < data.periods().size(); i++) {
|
||||
var period = data.periods().get(i); String unit = VoteStreaksSnapshot.unit(period.type());
|
||||
holder.inventory.setItem(11 + i * 2, item(Material.CLOCK, "&6" + period.type() + " streak",
|
||||
List.of("&7Current: &e" + period.current() + " " + unit, "&7Best: &e" + period.best() + " " + unit)));
|
||||
}
|
||||
List<VoteStreaksSnapshot.Milestone> milestones = data.page(holder.page);
|
||||
for (int i = 0; i < milestones.size(); i++) {
|
||||
var milestone = milestones.get(i); String unit = VoteStreaksSnapshot.unit(milestone.type());
|
||||
var lore = new ArrayList<String>();
|
||||
lore.add("&7Current: &e" + milestone.current() + " " + unit);
|
||||
lore.add("&7Best: &e" + milestone.best() + " " + unit);
|
||||
lore.add("&7Milestone: &e" + milestone.threshold() + " " + unit);
|
||||
lore.add("&7Votes per period: &e" + milestone.votesRequired());
|
||||
lore.add(milestone.recurring() ? "&7Repeats every " + milestone.threshold() + " " + unit + "; next: " + milestone.nextThreshold()
|
||||
: milestone.current() >= milestone.threshold() ? "&aCurrent streak meets this milestone" : "&7Upcoming milestone");
|
||||
lore.add("&7Progress only; this does not claim rewards.");
|
||||
holder.inventory.setItem(20 + i, item(Material.PAPER, "&e" + milestone.id(), lore));
|
||||
}
|
||||
if (milestones.isEmpty()) holder.inventory.setItem(22, item(Material.PAPER, "&7No enabled streak milestones",
|
||||
List.of("&7Configure VoteStreaks in SpecialRewards.yml.")));
|
||||
holder.inventory.setItem(45, item(Material.ARROW, "&ePrevious", List.of()));
|
||||
holder.inventory.setItem(49, item(Material.BARRIER, "&cClose", List.of()));
|
||||
holder.inventory.setItem(50, item(Material.CLOCK, "&eRefresh", List.of("&7Read current streaks again.")));
|
||||
holder.inventory.setItem(53, item(Material.ARROW, "&eNext", List.of("&7Page " + (holder.page + 1) + "/" + data.pages(),
|
||||
data.truncated() ? "&7Showing the first 512 enabled milestones." : "&7Read-only streak progression.")));
|
||||
session.view = holder;
|
||||
if (!current(session) || !permitted(session.player)) return;
|
||||
session.player.openInventory(holder.inventory);
|
||||
if (!current(session) && session.player.getOpenInventory().getTopInventory() == holder.inventory)
|
||||
session.player.closeInventory();
|
||||
if (session.player.getOpenInventory().getTopInventory() != holder.inventory) sessions.remove(session.owner, session);
|
||||
}
|
||||
private static ItemStack item(Material material, String name, List<String> lore) {
|
||||
ItemStack item = new ItemStack(material); ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(MessageAPI.colorize(name));
|
||||
meta.setLore(lore.stream().map(MessageAPI::colorize).toList()); item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
|
||||
public void inventoryOpen(InventoryOpenEvent event) {
|
||||
if (!(event.getPlayer() instanceof Player player)) return;
|
||||
Holder holder = holder(event.getInventory());
|
||||
if (holder == null) return;
|
||||
if (holder.session.player != player || !current(holder.session) || holder.session.view != holder || !permitted(player)) {
|
||||
event.setCancelled(true); return;
|
||||
}
|
||||
if (event.isCancelled()) sessions.remove(holder.session.owner, holder.session);
|
||||
}
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void unrelatedInventoryOpen(InventoryOpenEvent event) {
|
||||
if (!event.isCancelled() && holder(event.getInventory()) == null && event.getPlayer() instanceof Player player)
|
||||
sessions.remove(player.getUniqueId());
|
||||
}
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
|
||||
public void click(InventoryClickEvent event) {
|
||||
Holder holder = holder(event.getView().getTopInventory());
|
||||
if (holder == null) return;
|
||||
boolean cancelled = event.isCancelled(); event.setCancelled(true);
|
||||
if (cancelled || event.getWhoClicked() != holder.session.player || !current(holder.session)
|
||||
|| holder.session.view != holder || !permitted(holder.session.player)) return;
|
||||
// Only ordinary top-inventory clicks navigate; shift/number/drop actions cannot move previews.
|
||||
if (event.getRawSlot() < 0 || event.getRawSlot() >= 54 || !event.isLeftClick() || event.isShiftClick()) return;
|
||||
switch (event.getRawSlot()) {
|
||||
case 45 -> navigate(holder, holder.page - 1);
|
||||
case 53 -> navigate(holder, holder.page + 1);
|
||||
case 49 -> { sessions.remove(holder.session.owner, holder.session); closeView(holder.session); }
|
||||
case 50 -> begin(holder.session.player);
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
|
||||
public void drag(InventoryDragEvent event) {
|
||||
if (holder(event.getView().getTopInventory()) != null) event.setCancelled(true);
|
||||
}
|
||||
@EventHandler public void inventoryClose(InventoryCloseEvent event) {
|
||||
Holder holder = holder(event.getInventory());
|
||||
if (holder != null && holder.session.view == holder) sessions.remove(holder.session.owner, holder.session);
|
||||
else if (event.getPlayer() instanceof Player player) {
|
||||
Session pending = sessions.get(player.getUniqueId());
|
||||
if (pending != null && pending.view == null && pending.previous == event.getInventory()) sessions.remove(pending.owner, pending);
|
||||
}
|
||||
}
|
||||
@EventHandler public void quit(PlayerQuitEvent event) { sessions.remove(event.getPlayer().getUniqueId()); }
|
||||
@EventHandler public void worldChange(PlayerChangedWorldEvent event) { end(event.getPlayer()); }
|
||||
@EventHandler public void death(PlayerDeathEvent event) { end(event.getEntity()); }
|
||||
private void end(Player player) { Session session = sessions.remove(player.getUniqueId()); if (session != null) closeView(session); }
|
||||
private boolean retire(Session session) {
|
||||
if (!sessions.remove(session.owner, session)) return false;
|
||||
closeView(session); return true;
|
||||
}
|
||||
private void closeView(Session session) {
|
||||
if (session.view == null && !session.ownsPrevious) return;
|
||||
Runnable cleanup = () -> {
|
||||
// A queued cleanup must never close an inventory adopted by a newer request.
|
||||
Session replacement = sessions.get(session.owner);
|
||||
if (replacement != null && replacement != session) return;
|
||||
Inventory top = session.player.getOpenInventory().getTopInventory();
|
||||
if ((session.view != null && top == session.view.inventory)
|
||||
|| (session.ownsPrevious && top == session.previous)) session.player.closeInventory();
|
||||
};
|
||||
// Bukkit rejects scheduling after disable. Its primary thread already owns this interaction.
|
||||
if (closed && ownsForShutdown(Bukkit.class, session.player)) cleanup.run();
|
||||
else onPlayer(session.player, cleanup, () -> { });
|
||||
}
|
||||
/** Public Folia ownership also recognizes its stopped-region shutdown owner. */
|
||||
static boolean ownsForShutdown(Class<?> platform, Player player) {
|
||||
try {
|
||||
return Boolean.TRUE.equals(platform.getMethod("isOwnedByCurrentRegion", org.bukkit.entity.Entity.class).invoke(null, player));
|
||||
} catch (NoSuchMethodException legacy) {
|
||||
try {
|
||||
Class.forName("io.papermc.paper.threadedregions.RegionizedServer", false, VoteStreaksMenu.class.getClassLoader());
|
||||
return false; // A regional server without its ownership API must fail closed.
|
||||
} catch (ClassNotFoundException ordinaryServer) { return Bukkit.isPrimaryThread(); }
|
||||
catch (LinkageError unavailable) { return false; }
|
||||
} catch (ReflectiveOperationException | LinkageError unavailable) { return false; }
|
||||
}
|
||||
public void suspend() { reloadDepth.incrementAndGet(); reset(); }
|
||||
public void resume() { reloadDepth.updateAndGet(value -> Math.max(0, value - 1)); }
|
||||
public void reset() {
|
||||
List<Session> old;
|
||||
synchronized (sessions) { old = List.copyOf(sessions.values()); sessions.clear(); }
|
||||
old.forEach(this::closeView);
|
||||
}
|
||||
@Override public void close() { synchronized (sessions) { closed = true; } reset(); }
|
||||
int sessionCount() { return sessions.size(); }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.bencodez.votingplugin.streaks;
|
||||
|
||||
import java.util.List;
|
||||
import com.bencodez.votingplugin.specialrewards.votestreak.VoteStreakType;
|
||||
|
||||
/** Read-only observations of existing state; never evidence that a reward was delivered. */
|
||||
public record VoteStreaksSnapshot(List<Period> periods, List<Milestone> milestones, boolean truncated) {
|
||||
public static final int MAX_MILESTONES = 512;
|
||||
public static final int PAGE_SIZE = 5;
|
||||
public VoteStreaksSnapshot {
|
||||
periods = List.copyOf(periods);
|
||||
milestones = List.copyOf(milestones);
|
||||
if (periods.size() != 3 || milestones.size() > MAX_MILESTONES) throw new IllegalArgumentException("Invalid streak snapshot size");
|
||||
}
|
||||
public record Period(VoteStreakType type, int current, int best) {
|
||||
public Period { current = Math.max(0, current); best = Math.max(current, best); }
|
||||
}
|
||||
public record Milestone(String id, VoteStreakType type, int current, int best,
|
||||
int threshold, int votesRequired, boolean recurring) {
|
||||
public Milestone {
|
||||
current = Math.max(0, current); best = Math.max(current, best);
|
||||
threshold = Math.max(1, threshold); votesRequired = Math.max(1, votesRequired);
|
||||
}
|
||||
public long nextThreshold() {
|
||||
return recurring ? ((long) current / threshold + 1L) * threshold : threshold;
|
||||
}
|
||||
}
|
||||
public int pages() { return Math.max(1, (milestones.size() + PAGE_SIZE - 1) / PAGE_SIZE); }
|
||||
public int clampPage(int page) { return Math.max(0, Math.min(page, pages() - 1)); }
|
||||
public List<Milestone> page(int page) {
|
||||
int start = clampPage(page) * PAGE_SIZE;
|
||||
return milestones.subList(start, Math.min(start + PAGE_SIZE, milestones.size()));
|
||||
}
|
||||
public static String unit(VoteStreakType type) {
|
||||
return switch (type) { case DAILY -> "days"; case WEEKLY -> "weeks"; case MONTHLY -> "months"; };
|
||||
}
|
||||
}
|
||||
+11
-2
@@ -49,8 +49,15 @@ public class TimeQueueHandler implements Listener {
|
||||
* @param voteSiteName the vote site name
|
||||
*/
|
||||
public void addVote(String voteUsername, String voteSiteName) {
|
||||
timeChangeQueue.add(new VoteTimeQueue(voteUsername, voteSiteName,
|
||||
LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
|
||||
addVote(voteUsername, voteSiteName, 0L);
|
||||
}
|
||||
|
||||
/** Retain original ingress order only for this process; legacy callers have unknown order. */
|
||||
public void addVote(String voteUsername, String voteSiteName, long observationOrder) {
|
||||
VoteTimeQueue vote = new VoteTimeQueue(voteUsername, voteSiteName,
|
||||
LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
|
||||
vote.setBackendObservationOrder(observationOrder);
|
||||
timeChangeQueue.add(vote);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,6 +123,8 @@ public class TimeQueueHandler implements Listener {
|
||||
plugin.getVoteSiteManager().getVoteSite(plugin.getVoteSiteManager().getVoteSiteName(true, vote.getService()), true), vote.getName(),
|
||||
vote.getService(), true);
|
||||
voteEvent.setTime(vote.getTime());
|
||||
voteEvent.setTimeQueuedLocalSessionDelivery(true);
|
||||
voteEvent.setBackendObservationOrder(vote.getBackendObservationOrder());
|
||||
plugin.getServer().getPluginManager().callEvent(voteEvent);
|
||||
|
||||
if (voteEvent.isCancelled()) {
|
||||
|
||||
@@ -27,6 +27,10 @@ public class VoteTimeQueue {
|
||||
@Getter
|
||||
@Setter
|
||||
private long time;
|
||||
/** Process-local original ingress order; intentionally excluded from persisted cache rows. */
|
||||
@Getter
|
||||
@Setter
|
||||
private transient long backendObservationOrder;
|
||||
@Getter
|
||||
@Setter
|
||||
private UUID voteId;
|
||||
|
||||
@@ -2,7 +2,11 @@ package com.bencodez.votingplugin.user;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
import java.util.UUID;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.bencodez.votingplugin.placeholders.PlaceholderPlayerPresence.LiveIdentity;
|
||||
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -22,7 +26,8 @@ import com.bencodez.votingplugin.topvoter.TopVoter;
|
||||
* Manages VotingPlugin user operations and data.
|
||||
*/
|
||||
public class UserManager {
|
||||
private VotingPluginMain plugin;
|
||||
private VotingPluginMain plugin;
|
||||
private final Set<UUID> warnedOfflineUuidPlayers = ConcurrentHashMap.newKeySet();
|
||||
private boolean sharedPointTransferRecoveryScheduled;
|
||||
|
||||
/**
|
||||
@@ -260,11 +265,18 @@ public class UserManager {
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public VotingPluginUser getVotingPluginUser(UUID uuid) {
|
||||
return new VotingPluginUser(plugin, uuid);
|
||||
LiveIdentity identity = offlineLiveIdentity(uuid);
|
||||
if (identity == null) return new VotingPluginUser(plugin, uuid);
|
||||
VotingPluginUser user = new VotingPluginUser(plugin, identity.storageUuid(), false);
|
||||
user.setPlayerName(identity.playerName());
|
||||
warnOfflineUuidCorrection(uuid, identity);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a VotingPluginUser by UUID with optional name loading.
|
||||
* Gets a VotingPluginUser by exact storage UUID with optional name loading.
|
||||
* This overload preserves the supplied key for storage iteration, recovery
|
||||
* and administrative operations, even when loadName is true.
|
||||
* @param uuid the player UUID
|
||||
* @param loadName whether to load the player name
|
||||
* @return the VotingPluginUser
|
||||
@@ -285,6 +297,24 @@ public class UserManager {
|
||||
return new VotingPluginUser(plugin, uuid, playerName);
|
||||
}
|
||||
|
||||
/** Resolve only immutable live identity captured by the presence lifecycle. */
|
||||
private LiveIdentity offlineLiveIdentity(UUID uuid) {
|
||||
if (plugin.getOptions().isOnlineMode()) return null;
|
||||
var presence = plugin.getPlaceholderPlayerPresence();
|
||||
LiveIdentity identity = presence == null ? null : presence.liveIdentity(uuid);
|
||||
if (identity == null || identity.serverUuid() == null || identity.storageUuid() == null
|
||||
|| identity.playerName() == null || identity.playerName().isBlank()) return null;
|
||||
return identity;
|
||||
}
|
||||
|
||||
private void warnOfflineUuidCorrection(UUID uuid, LiveIdentity identity) {
|
||||
if (!uuid.equals(identity.storageUuid()) && warnedOfflineUuidPlayers.add(identity.storageUuid())) {
|
||||
plugin.getLogger().warning("Offline UUID compatibility workaround used for " + identity.playerName()
|
||||
+ ". Server UUID: " + identity.serverUuid() + " -> VotingPlugin UUID: " + identity.storageUuid()
|
||||
+ ". Applied for third-party compatibility: a plugin may be using the server UUID instead of VotingPlugin's UUID.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges old players with no voting data.
|
||||
*/
|
||||
|
||||
@@ -1003,16 +1003,45 @@ public class VotingPluginUser extends com.bencodez.advancedcore.api.user.Advance
|
||||
boolean wasOnline, boolean broadcast, int num, boolean queuedProxyVote,
|
||||
boolean proxyDelayValidationKnown, boolean proxyQueueClassificationKnown, UUID proxyVoteId,
|
||||
boolean targetedProxyVote) {
|
||||
bungeeVotePluginMessagingEvent(service, time, text, setTotals, wasOnline, broadcast, num, queuedProxyVote,
|
||||
proxyDelayValidationKnown, proxyQueueClassificationKnown, proxyVoteId, targetedProxyVote);
|
||||
bungeeVotePluginMessaging(service, time, text, setTotals, wasOnline, broadcast, num, queuedProxyVote,
|
||||
proxyDelayValidationKnown, proxyQueueClassificationKnown, proxyVoteId, targetedProxyVote, false);
|
||||
}
|
||||
|
||||
/** Dispatches the proxy vote and returns its final cancellation state; null when proxy mode is disabled. */
|
||||
public PlayerVoteEvent bungeeVotePluginMessagingEvent(String service, long time, VoteTotalsSnapshot text, boolean setTotals,
|
||||
/** Supplies guide-only provenance independently of the established reward/delay flags. */
|
||||
public void bungeeVotePluginMessaging(String service, long time, VoteTotalsSnapshot text, boolean setTotals,
|
||||
boolean wasOnline, boolean broadcast, int num, boolean queuedProxyVote,
|
||||
boolean proxyDelayValidationKnown, boolean proxyQueueClassificationKnown, UUID proxyVoteId,
|
||||
boolean targetedProxyVote) {
|
||||
if (plugin.getBungeeSettings().isUseBungeecoord()) {
|
||||
boolean targetedProxyVote, boolean unconfirmedProxySessionDelivery) {
|
||||
bungeeVotePluginMessaging(service, time, text, setTotals, wasOnline, broadcast, num, queuedProxyVote,
|
||||
proxyDelayValidationKnown, proxyQueueClassificationKnown, proxyVoteId, targetedProxyVote,
|
||||
unconfirmedProxySessionDelivery, true);
|
||||
}
|
||||
|
||||
/** Guide-only real/test classification; preserve the established normal vote pipeline semantics. */
|
||||
public void bungeeVotePluginMessaging(String service, long time, VoteTotalsSnapshot text, boolean setTotals,
|
||||
boolean wasOnline, boolean broadcast, int num, boolean queuedProxyVote,
|
||||
boolean proxyDelayValidationKnown, boolean proxyQueueClassificationKnown, UUID proxyVoteId,
|
||||
boolean targetedProxyVote, boolean unconfirmedProxySessionDelivery, boolean sessionRealVote) {
|
||||
bungeeVotePluginMessagingEvent(service, time, text, setTotals, wasOnline, broadcast, num, queuedProxyVote,
|
||||
proxyDelayValidationKnown, proxyQueueClassificationKnown, proxyVoteId, targetedProxyVote,
|
||||
unconfirmedProxySessionDelivery, sessionRealVote);
|
||||
}
|
||||
|
||||
/** Dispatches a proxy vote with legacy provenance defaults and returns its cancellation state. */
|
||||
public PlayerVoteEvent bungeeVotePluginMessagingEvent(String service, long time, VoteTotalsSnapshot text, boolean setTotals,
|
||||
boolean wasOnline, boolean broadcast, int num, boolean queuedProxyVote,
|
||||
boolean proxyDelayValidationKnown, boolean proxyQueueClassificationKnown, UUID proxyVoteId,
|
||||
boolean targetedProxyVote) {
|
||||
return bungeeVotePluginMessagingEvent(service, time, text, setTotals, wasOnline, broadcast, num, queuedProxyVote,
|
||||
proxyDelayValidationKnown, proxyQueueClassificationKnown, proxyVoteId, targetedProxyVote, false, true);
|
||||
}
|
||||
|
||||
/** Preserves guide-only occurrence provenance while exposing the final cancellation state. */
|
||||
public PlayerVoteEvent bungeeVotePluginMessagingEvent(String service, long time, VoteTotalsSnapshot text, boolean setTotals,
|
||||
boolean wasOnline, boolean broadcast, int num, boolean queuedProxyVote,
|
||||
boolean proxyDelayValidationKnown, boolean proxyQueueClassificationKnown, UUID proxyVoteId,
|
||||
boolean targetedProxyVote, boolean unconfirmedProxySessionDelivery, boolean sessionRealVote) {
|
||||
if (plugin.getBungeeSettings().isUseBungeecoord()) {
|
||||
plugin.debug("Pluginmessaging vote for " + getPlayerName() + " on " + service);
|
||||
|
||||
PlayerVoteEvent voteEvent = new PlayerVoteEvent(plugin.getVoteSiteManager().getVoteSite(service, true),
|
||||
@@ -1031,6 +1060,8 @@ public class VotingPluginUser extends com.bencodez.advancedcore.api.user.Advance
|
||||
voteEvent.setProxyQueueClassificationKnown(proxyQueueClassificationKnown);
|
||||
voteEvent.setProxyDelayValidationKnown(proxyDelayValidationKnown);
|
||||
voteEvent.setTargetedProxyVote(targetedProxyVote);
|
||||
voteEvent.setUnconfirmedProxySessionDelivery(unconfirmedProxySessionDelivery);
|
||||
voteEvent.setProxySessionRealVote(sessionRealVote);
|
||||
plugin.getServer().getPluginManager().callEvent(voteEvent);
|
||||
return voteEvent;
|
||||
}
|
||||
@@ -1078,7 +1109,11 @@ public class VotingPluginUser extends com.bencodez.advancedcore.api.user.Advance
|
||||
* @return true, if the user can vote on the site
|
||||
*/
|
||||
public boolean canVoteSite(VoteSite voteSite) {
|
||||
long time = getTime(voteSite);
|
||||
return canVoteSite(voteSite, getTime(voteSite));
|
||||
}
|
||||
|
||||
/** Same cooldown decision using an already loaded last-vote snapshot; performs no storage read. */
|
||||
public boolean canVoteSite(VoteSite voteSite, long time) {
|
||||
if (time == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ DebugLevel: NONE
|
||||
GiveDefaultPermission: true
|
||||
|
||||
# If true, plugin will automatically generate votesites upon getting a vote
|
||||
# Disable after setup; a small periodic console reminder warns while enabled.
|
||||
AutoCreateVoteSites: true
|
||||
|
||||
# Geyser/Bedrock player support
|
||||
@@ -211,6 +212,14 @@ UseVoteGUIMainCommand: false
|
||||
# Does not backup MySQL data
|
||||
CreateBackups: true
|
||||
|
||||
# Player root command and standalone player aliases; admin commands remain available.
|
||||
# These settings require a server restart; /av reload does not change registration.
|
||||
VoteCommandEnabled: true
|
||||
# Root label without a slash or namespace; 1-64 letters, digits, underscores or hyphens.
|
||||
# Reserved/invalid names use vote. A foreign bare-name conflict uses votingplugin:<name>.
|
||||
# Standalone aliases such as /votegui and /vgui keep their existing names.
|
||||
VoteCommandName: vote
|
||||
|
||||
# Load command aliases, such as /avgui
|
||||
# Requires restart to take effect
|
||||
LoadCommandAliases: true
|
||||
@@ -1256,3 +1265,9 @@ DisableUpdateChecking: false
|
||||
# Disable checking permissions on tab complete
|
||||
# Could increase performance slightly, but probably not
|
||||
DisableAdvancedTab: false
|
||||
|
||||
# Observation-only chat guide; normal /vote and reward processing remain unchanged.
|
||||
# Progress is local, expires after this interval, and resets on reload/restart.
|
||||
GuidedVotingSession:
|
||||
Enabled: false
|
||||
TimeoutMinutes: 30
|
||||
@@ -294,6 +294,10 @@ CHEST:
|
||||
Lore: '&c&lBest: &c%Best%'
|
||||
Amount: 1
|
||||
|
||||
# Separate read-only /vote streaks progression menu.
|
||||
VoteStreaks:
|
||||
Name: '&6Vote streaks'
|
||||
|
||||
VoteStreak:
|
||||
Name: 'VoteStreak: %player%'
|
||||
BackButton: false
|
||||
|
||||
@@ -195,3 +195,12 @@ permissions:
|
||||
VotingPlugin.Login.RemindVotes.All:
|
||||
description: Remind if player can vote on all votesites, rather than any
|
||||
default: false
|
||||
VotingPlugin.Commands.Vote.Session:
|
||||
description: Start or navigate a guided voting session
|
||||
default: false
|
||||
|
||||
VotingPlugin.Commands.Vote.Streaks:
|
||||
description: View your read-only vote streak progression
|
||||
default: op
|
||||
children:
|
||||
VotingPlugin.Commands.Vote: true
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.bencodez.votingplugin.tests;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
import com.bencodez.votingplugin.config.Config;
|
||||
import com.bencodez.votingplugin.maintenance.AutoCreateVoteSitesReminderService.Settings;
|
||||
|
||||
class AutoCreateVoteSitesReminderConfigTest {
|
||||
|
||||
@Test
|
||||
void bundledConfigHasOnlyExistingAutoCreateSetting() throws Exception {
|
||||
var resource = VotingPluginMain.class.getClassLoader().getResourceAsStream("Config.yml");
|
||||
assertNotNull(resource);
|
||||
try (var reader = new InputStreamReader(resource, StandardCharsets.UTF_8)) {
|
||||
YamlConfiguration config = YamlConfiguration.loadConfiguration(reader);
|
||||
assertTrue(config.getBoolean("AutoCreateVoteSites"));
|
||||
assertFalse(config.contains("AutoCreateVoteSitesReminder"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void enabledAutoCreationGetsStartupAndTwoDayNagWithoutExtraSettings() throws Exception {
|
||||
Config config = mock(Config.class);
|
||||
when(config.isAutoCreateVoteSites()).thenReturn(true);
|
||||
Settings settings = settings(config);
|
||||
assertTrue(settings.enabled());
|
||||
assertTrue(settings.autoCreateEnabled());
|
||||
assertEquals(0, settings.afterMillis());
|
||||
assertEquals(TimeUnit.HOURS.toMillis(48), settings.repeatMillis());
|
||||
verify(config, never()).getData();
|
||||
}
|
||||
|
||||
@Test
|
||||
void disablingExistingAutoCreateSettingStopsNag() throws Exception {
|
||||
Config config = mock(Config.class);
|
||||
when(config.isAutoCreateVoteSites()).thenReturn(false);
|
||||
assertFalse(settings(config).autoCreateEnabled());
|
||||
verify(config, never()).getData();
|
||||
}
|
||||
|
||||
private static Settings settings(Config config) throws Exception {
|
||||
VotingPluginMain plugin = mock(VotingPluginMain.class, CALLS_REAL_METHODS);
|
||||
Field field = VotingPluginMain.class.getDeclaredField("configFile");
|
||||
field.setAccessible(true);
|
||||
ReflectiveAccess.set(field, plugin, config);
|
||||
Method method = VotingPluginMain.class.getDeclaredMethod("autoCreateVoteSitesReminderSettings");
|
||||
method.setAccessible(true);
|
||||
return (Settings) ReflectiveAccess.invoke(method, plugin, new Object[]{});
|
||||
}
|
||||
}
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
package com.bencodez.votingplugin.tests;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.bencodez.votingplugin.core.maintenance.AutoCreateVoteSitesReminderState;
|
||||
import com.bencodez.votingplugin.core.maintenance.AutoCreateVoteSitesReminderState.Candidate;
|
||||
import com.bencodez.votingplugin.core.maintenance.AutoCreateVoteSitesReminderState.Snapshot;
|
||||
|
||||
class AutoCreateVoteSitesReminderStateTest {
|
||||
|
||||
@Test
|
||||
void firstReminderUsesStartupThresholdAndFortyEightHourRepeatDefault() {
|
||||
long threshold = AutoCreateVoteSitesReminderState.DEFAULT_THRESHOLD_MILLIS;
|
||||
assertEquals(0, threshold);
|
||||
assertEquals(TimeUnit.HOURS.toMillis(48), AutoCreateVoteSitesReminderState.DEFAULT_REPEAT_MILLIS);
|
||||
AutoCreateVoteSitesReminderState state = fresh(0);
|
||||
Candidate first = state.propose(0, true, true, threshold,
|
||||
AutoCreateVoteSitesReminderState.DEFAULT_REPEAT_MILLIS);
|
||||
assertTrue(first.reminderDue());
|
||||
assertEquals(new Snapshot(0, 0), first.snapshot());
|
||||
state.commit(first);
|
||||
Candidate boundary = state.propose(nanos(AutoCreateVoteSitesReminderState.DEFAULT_REPEAT_MILLIS), true, true,
|
||||
threshold, AutoCreateVoteSitesReminderState.DEFAULT_REPEAT_MILLIS);
|
||||
assertTrue(boundary.reminderDue());
|
||||
assertEquals(new Snapshot(AutoCreateVoteSitesReminderState.DEFAULT_REPEAT_MILLIS,
|
||||
AutoCreateVoteSitesReminderState.DEFAULT_REPEAT_MILLIS), boundary.snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
void disabledFlagsSuppressOnlyReminderAndKeepAccruingRuntime() {
|
||||
boolean[][] disabled = { { false, false }, { false, true }, { true, false } };
|
||||
for (boolean[] flags : disabled) {
|
||||
AutoCreateVoteSitesReminderState state = fresh(0);
|
||||
Candidate candidate = state.propose(nanos(1500), flags[0], flags[1], 1000, 500);
|
||||
assertFalse(candidate.reminderDue());
|
||||
assertEquals(new Snapshot(1500, -1), candidate.snapshot());
|
||||
state.commit(candidate);
|
||||
Candidate enabled = state.propose(nanos(1500), true, true, 1000, 500);
|
||||
assertTrue(enabled.reminderDue());
|
||||
assertEquals(new Snapshot(1500, 1500), enabled.snapshot());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartLoadsCumulativeRuntimeAndExcludesOfflineTime() {
|
||||
AutoCreateVoteSitesReminderState previous = fresh(0);
|
||||
previous.commit(previous.propose(nanos(900), false, false, 1000, 500));
|
||||
long restartClock = nanos(TimeUnit.DAYS.toMillis(30));
|
||||
AutoCreateVoteSitesReminderState restarted = new AutoCreateVoteSitesReminderState(previous.snapshot(),
|
||||
restartClock);
|
||||
assertFalse(restarted.propose(restartClock, true, true, 1000, 500).reminderDue());
|
||||
Candidate before = restarted.propose(restartClock + nanos(99), true, true, 1000, 500);
|
||||
assertEquals(new Snapshot(999, -1), before.snapshot());
|
||||
assertFalse(before.reminderDue());
|
||||
restarted.commit(before);
|
||||
Candidate boundary = restarted.propose(restartClock + nanos(100), true, true, 1000, 500);
|
||||
assertEquals(new Snapshot(1000, 1000), boundary.snapshot());
|
||||
assertTrue(boundary.reminderDue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartPreservesReminderCooldownWithFreshClockAnchor() {
|
||||
AutoCreateVoteSitesReminderState state = new AutoCreateVoteSitesReminderState(new Snapshot(1000, 1000),
|
||||
-nanos(5000));
|
||||
assertFalse(state.propose(-nanos(4501), true, true, 1000, 500).reminderDue());
|
||||
assertTrue(state.propose(-nanos(4500), true, true, 1000, 500).reminderDue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatBoundaryIsInclusiveAndAdvancesOnlyAfterCommit() {
|
||||
AutoCreateVoteSitesReminderState state = new AutoCreateVoteSitesReminderState(new Snapshot(1000, 1000), 0);
|
||||
Candidate before = state.propose(nanos(499), true, true, 1000, 500);
|
||||
assertFalse(before.reminderDue());
|
||||
state.commit(before);
|
||||
Candidate boundary = state.propose(nanos(500), true, true, 1000, 500);
|
||||
assertTrue(boundary.reminderDue());
|
||||
assertEquals(new Snapshot(1499, 1000), state.snapshot());
|
||||
state.commit(boundary);
|
||||
assertEquals(new Snapshot(1500, 1500), state.snapshot());
|
||||
assertFalse(state.propose(nanos(999), true, true, 1000, 500).reminderDue());
|
||||
assertTrue(state.propose(nanos(1000), true, true, 1000, 500).reminderDue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedPersistenceLeavesUptimeAndReminderRetryableWithoutDoubleCounting() {
|
||||
AutoCreateVoteSitesReminderState state = fresh(0);
|
||||
Candidate failedWrite = state.propose(nanos(1000), true, true, 1000, 500);
|
||||
assertTrue(failedWrite.reminderDue());
|
||||
// The storage owner must omit commit when its write fails.
|
||||
assertEquals(new Snapshot(0, -1), state.snapshot());
|
||||
Candidate sameClockRetry = state.propose(nanos(1000), true, true, 1000, 500);
|
||||
assertEquals(failedWrite.snapshot(), sameClockRetry.snapshot());
|
||||
Candidate laterRetry = state.propose(nanos(1200), true, true, 1000, 500);
|
||||
assertTrue(laterRetry.reminderDue());
|
||||
assertEquals(new Snapshot(1200, 1200), laterRetry.snapshot());
|
||||
state.commit(laterRetry);
|
||||
assertThrows(IllegalArgumentException.class, () -> state.commit(failedWrite));
|
||||
assertThrows(IllegalArgumentException.class, () -> state.commit(sameClockRetry));
|
||||
Candidate next = state.propose(nanos(1300), true, true, 1000, 500);
|
||||
assertEquals(new Snapshot(1300, 1200), next.snapshot());
|
||||
assertFalse(next.reminderDue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedNonReminderCheckpointDoesNotMintRuntimeOnRetry() {
|
||||
AutoCreateVoteSitesReminderState state = fresh(0);
|
||||
state.propose(nanos(300), false, false, 1000, 500);
|
||||
Candidate retry = state.propose(nanos(400), false, false, 1000, 500);
|
||||
assertEquals(new Snapshot(400, -1), retry.snapshot());
|
||||
state.commit(retry);
|
||||
assertEquals(new Snapshot(500, -1), state.propose(nanos(500), false, false, 1000, 500).snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
void staleAndDuplicateCommitsAreRejectedWithoutPublishingProgress() {
|
||||
AutoCreateVoteSitesReminderState state = fresh(0);
|
||||
Candidate first = state.propose(nanos(100), false, false, 1000, 500);
|
||||
Candidate sibling = state.propose(nanos(200), false, false, 1000, 500);
|
||||
state.commit(first);
|
||||
assertThrows(IllegalArgumentException.class, () -> state.commit(first));
|
||||
assertThrows(IllegalArgumentException.class, () -> state.commit(sibling));
|
||||
assertEquals(new Snapshot(100, -1), state.snapshot());
|
||||
Candidate retry = state.propose(nanos(200), false, false, 1000, 500);
|
||||
state.commit(retry);
|
||||
assertEquals(new Snapshot(200, -1), state.snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidateFromAnotherStateIsRejectedEvenWithIdenticalSnapshots() {
|
||||
AutoCreateVoteSitesReminderState state = fresh(0);
|
||||
AutoCreateVoteSitesReminderState other = fresh(0);
|
||||
Candidate foreign = other.propose(nanos(1000), true, true, 1000, 500);
|
||||
assertThrows(IllegalArgumentException.class, () -> state.commit(foreign));
|
||||
assertEquals(new Snapshot(0, -1), state.snapshot());
|
||||
other.commit(foreign);
|
||||
assertEquals(new Snapshot(1000, 1000), other.snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
void changingThresholdAndRepeatKeepsExistingRuntimeAndReminderMarker() {
|
||||
AutoCreateVoteSitesReminderState state = fresh(0);
|
||||
state.commit(state.propose(nanos(1500), true, true, 2000, 1000));
|
||||
Candidate loweredThreshold = state.propose(nanos(1500), true, true, 1000, 1000);
|
||||
assertTrue(loweredThreshold.reminderDue());
|
||||
state.commit(loweredThreshold);
|
||||
assertFalse(state.propose(nanos(2000), true, true, 3000, 100).reminderDue());
|
||||
assertFalse(state.propose(nanos(2000), true, true, 1000, 1000).reminderDue());
|
||||
Candidate loweredRepeat = state.propose(nanos(2000), true, true, 1000, 500);
|
||||
assertTrue(loweredRepeat.reminderDue());
|
||||
assertEquals(new Snapshot(2000, 2000), loweredRepeat.snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
void disablingAfterReminderDoesNotClearItsMarker() {
|
||||
AutoCreateVoteSitesReminderState state = new AutoCreateVoteSitesReminderState(new Snapshot(1000, 1000), 0);
|
||||
Candidate disabled = state.propose(nanos(600), false, false, 1000, 500);
|
||||
state.commit(disabled);
|
||||
assertEquals(new Snapshot(1600, 1000), state.snapshot());
|
||||
assertFalse(disabled.reminderDue());
|
||||
assertTrue(state.propose(nanos(600), true, true, 1000, 500).reminderDue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void negativeNanoReadingsAccruePositiveElapsedRuntime() {
|
||||
AutoCreateVoteSitesReminderState state = fresh(-nanos(1000));
|
||||
Candidate candidate = state.propose(-nanos(900), false, false, 1000, 500);
|
||||
assertEquals(new Snapshot(100, -1), candidate.snapshot());
|
||||
state.commit(candidate);
|
||||
assertEquals(new Snapshot(200, -1), state.propose(-nanos(800), false, false, 1000, 500).snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
void signedNanoClockWrapAccruesElapsedTime() {
|
||||
AutoCreateVoteSitesReminderState state = fresh(Long.MAX_VALUE - 500_000);
|
||||
Candidate wrapped = state.propose(Long.MIN_VALUE + 499_999, true, true, 1, 1);
|
||||
assertTrue(wrapped.reminderDue());
|
||||
assertEquals(new Snapshot(1, 1), wrapped.snapshot());
|
||||
state.commit(wrapped);
|
||||
assertEquals(new Snapshot(2, 2), state.propose(Long.MIN_VALUE + 1_499_999, true, true, 1, 1).snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
void backwardClockDoesNotMoveAnchorOrMintElapsedRuntime() {
|
||||
AutoCreateVoteSitesReminderState state = fresh(nanos(1000));
|
||||
Candidate backward = state.propose(nanos(900), true, true, 1000, 500);
|
||||
assertEquals(new Snapshot(0, -1), backward.snapshot());
|
||||
state.commit(backward);
|
||||
assertEquals(new Snapshot(100, -1), state.propose(nanos(1100), false, false, 1000, 500).snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
void subMillisecondCheckpointsRetainRemainderAndNeverDoubleCount() {
|
||||
AutoCreateVoteSitesReminderState state = fresh(0);
|
||||
for (long clock : new long[] { 400_000, 800_000, 1_200_000, 1_600_000, 2_000_000 }) {
|
||||
state.commit(state.propose(clock, false, false, 1000, 500));
|
||||
}
|
||||
assertEquals(new Snapshot(2, -1), state.snapshot());
|
||||
state.commit(state.propose(2_000_000, false, false, 1000, 500));
|
||||
assertEquals(new Snapshot(2, -1), state.snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
void runtimeSaturatesWithoutOverflowAndReminderMarkerRemainsValid() {
|
||||
AutoCreateVoteSitesReminderState state = new AutoCreateVoteSitesReminderState(new Snapshot(Long.MAX_VALUE - 2, -1), 0);
|
||||
Candidate candidate = state.propose(nanos(10), true, true, 1000, 500);
|
||||
assertTrue(candidate.reminderDue());
|
||||
assertEquals(new Snapshot(Long.MAX_VALUE, Long.MAX_VALUE), candidate.snapshot());
|
||||
state.commit(candidate);
|
||||
Candidate later = state.propose(nanos(20), true, true, 1000, 500);
|
||||
assertEquals(new Snapshot(Long.MAX_VALUE, Long.MAX_VALUE), later.snapshot());
|
||||
assertFalse(later.reminderDue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void saturationDoesNotOverflowRepeatElapsedComparison() {
|
||||
AutoCreateVoteSitesReminderState state = new AutoCreateVoteSitesReminderState(new Snapshot(Long.MAX_VALUE - 1, 0), 0);
|
||||
Candidate candidate = state.propose(nanos(100), true, true,
|
||||
AutoCreateVoteSitesReminderState.MAX_INTERVAL_MILLIS, AutoCreateVoteSitesReminderState.MAX_INTERVAL_MILLIS);
|
||||
assertTrue(candidate.reminderDue());
|
||||
assertEquals(Long.MAX_VALUE, candidate.snapshot().totalRuntimeMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void snapshotsRejectNegativeRuntimeInvalidSentinelAndFutureReminderMarker() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new Snapshot(-1, -1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new Snapshot(0, -2));
|
||||
assertThrows(IllegalArgumentException.class, () -> new Snapshot(0, 1));
|
||||
assertThrows(IllegalArgumentException.class, () -> new Snapshot(Long.MAX_VALUE - 1, Long.MAX_VALUE));
|
||||
assertEquals(new Snapshot(0, -1), fresh(0).snapshot());
|
||||
assertEquals(0, new Snapshot(0, 0).lastReminderRuntimeMillis());
|
||||
assertEquals(Long.MAX_VALUE, new Snapshot(Long.MAX_VALUE, Long.MAX_VALUE).totalRuntimeMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void intervalsAllowStartupThresholdButRequirePositiveBoundedRepeatsEvenWhileDisabled() {
|
||||
AutoCreateVoteSitesReminderState state = fresh(0);
|
||||
for (long invalid : new long[] { -1, AutoCreateVoteSitesReminderState.MAX_INTERVAL_MILLIS + 1, Long.MAX_VALUE }) {
|
||||
assertThrows(IllegalArgumentException.class, () -> state.propose(0, false, false, invalid, 1));
|
||||
assertThrows(IllegalArgumentException.class, () -> state.propose(0, false, false, 1, invalid));
|
||||
}
|
||||
assertThrows(IllegalArgumentException.class, () -> state.propose(0, false, false, 0, 0));
|
||||
assertFalse(state.propose(0, false, false, 0, 1).reminderDue());
|
||||
assertEquals(new Snapshot(0, -1), state.snapshot());
|
||||
assertFalse(state.propose(0, true, true, AutoCreateVoteSitesReminderState.MAX_INTERVAL_MILLIS,
|
||||
AutoCreateVoteSitesReminderState.MAX_INTERVAL_MILLIS).reminderDue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullSnapshotAndNullCandidateAreRejected() {
|
||||
assertThrows(NullPointerException.class, () -> new AutoCreateVoteSitesReminderState(null, 0));
|
||||
AutoCreateVoteSitesReminderState state = fresh(0);
|
||||
assertThrows(NullPointerException.class, () -> state.commit(null));
|
||||
assertEquals(new Snapshot(0, -1), state.snapshot());
|
||||
}
|
||||
|
||||
private static AutoCreateVoteSitesReminderState fresh(long nowNanos) {
|
||||
return new AutoCreateVoteSitesReminderState(new Snapshot(0, AutoCreateVoteSitesReminderState.NEVER_REMINDED), nowNanos);
|
||||
}
|
||||
|
||||
private static long nanos(long millis) {
|
||||
return Math.multiplyExact(millis, 1_000_000);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
package com.bencodez.votingplugin.tests;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@@ -1418,7 +1420,7 @@ class CachedVoteBatchDurabilityTest {
|
||||
for (var pair : List.of(new Object[] {"useMySQL", true}, new Object[] {"voteCacheTable", serverTable},
|
||||
new Object[] {"onlineVoteCacheTable", onlineTable}, new Object[] {"timedVoteCacheTable", timedTable})) {
|
||||
Field field = VoteCacheHandler.class.getDeclaredField((String) pair[0]);
|
||||
field.setAccessible(true); field.set(handler, pair[1]);
|
||||
field.setAccessible(true); ReflectiveAccess.set(field, handler, pair[1]);
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
package com.bencodez.votingplugin.tests;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope;
|
||||
import com.bencodez.simpleapi.servercomm.global.GlobalMessageProxyHandler;
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
import com.bencodez.votingplugin.backendproxy.cache.ProcessedVoteCache;
|
||||
import com.bencodez.votingplugin.backendproxy.global.BackendGlobalDataSync;
|
||||
import com.bencodez.votingplugin.backendproxy.messaging.BackendProxyMessageRouter;
|
||||
import com.bencodez.votingplugin.backendproxy.presence.BackendPresenceManager;
|
||||
import com.bencodez.votingplugin.backendproxy.voteparty.BackendVotePartySync;
|
||||
import com.bencodez.votingplugin.config.BungeeSettings;
|
||||
import com.bencodez.votingplugin.core.vote.SharedVotePolicy;
|
||||
import com.bencodez.votingplugin.core.vote.SharedVoteProcessor;
|
||||
import com.bencodez.votingplugin.data.ServerData;
|
||||
import com.bencodez.votingplugin.events.PlayerVoteEvent;
|
||||
import com.bencodez.votingplugin.proxy.BungeeMethod;
|
||||
import com.bencodez.votingplugin.proxy.VoteTotalsSnapshot;
|
||||
import com.bencodez.votingplugin.proxy.VotingPluginProxy;
|
||||
import com.bencodez.votingplugin.proxy.VotingPluginWire;
|
||||
import com.bencodez.votingplugin.proxy.cache.VoteCacheHandler;
|
||||
import com.bencodez.votingplugin.proxy.multiproxy.MultiProxyHandler;
|
||||
import com.bencodez.votingplugin.user.UserManager;
|
||||
import com.bencodez.votingplugin.user.VotingPluginUser;
|
||||
import com.bencodez.votingplugin.votesites.VoteSite;
|
||||
import com.bencodez.votingplugin.votesites.VoteSiteManager;
|
||||
|
||||
class MultiProxyCooldownCompatibilityTest {
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicTest> reliableForwardingKeepsNormalCooldownTimeLocalToReceiver() {
|
||||
return Stream.of(100L, System.currentTimeMillis() + 86_400_000L)
|
||||
.map(senderTime -> DynamicTest.dynamicTest("sender time " + senderTime,
|
||||
() -> assertReceiverCooldownTime(senderTime)));
|
||||
}
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
void sourceFenceRepliesOnlyToConfiguredOriginAndOriginRelaysOnlyItsOwnSession() throws Exception {
|
||||
VotingPluginProxyTestImpl proxy = spy(new VotingPluginProxyTestImpl());
|
||||
proxy.setDataFolder(temporaryDirectory.toFile());
|
||||
when(proxy.getConfig().getProxyServerName()).thenReturn("Receiver");
|
||||
var backendReplies = new java.util.ArrayList<JsonEnvelope>();
|
||||
proxy.setGlobalMessageProxyHandlerForTest(new GlobalMessageProxyHandler() {
|
||||
@Override public void sendMessage(String server, int delay, JsonEnvelope envelope) {
|
||||
assertEquals("server1", server); backendReplies.add(envelope);
|
||||
}
|
||||
});
|
||||
proxy.loadMultiProxySupport();
|
||||
MultiProxyHandler handler = spy(proxy.getMultiProxyHandler());
|
||||
doReturn(com.bencodez.votingplugin.proxy.multiproxy.MultiProxyMethod.REDIS).when(handler).getMultiProxyMethod();
|
||||
doReturn(java.util.List.of("Origin")).when(handler).getProxyServers();
|
||||
doReturn("Receiver").when(handler).getMultiProxyServerName();
|
||||
var outgoing = new java.util.ArrayList<JsonEnvelope>();
|
||||
org.mockito.Mockito.doAnswer(i -> {
|
||||
assertEquals(java.util.List.of("Origin"), i.getArgument(1));
|
||||
outgoing.add(i.getArgument(0)); return true;
|
||||
}).when(handler).sendMultiProxyEnvelopeAccepted(any(JsonEnvelope.class), any(java.util.Collection.class));
|
||||
Method receive = MultiProxyHandler.class.getDeclaredMethod("handleEnvelope", JsonEnvelope.class);
|
||||
receive.setAccessible(true);
|
||||
var request = new VotingPluginWire.SessionFenceRequest(UUID.randomUUID(), UUID.randomUUID(), "server1", "Origin");
|
||||
receive.invoke(handler, VotingPluginWire.sessionFenceRequest(request));
|
||||
assertEquals(1, outgoing.size());
|
||||
var reply = VotingPluginWire.readSessionFenceReply(outgoing.get(0)); assertNotNull(reply);
|
||||
assertEquals(request, reply.request());
|
||||
assertTrue(reply.fence().newer(com.bencodez.votingplugin.core.session.ProxyVoteSequence.nextVoteId()));
|
||||
receive.invoke(handler, VotingPluginWire.sessionFenceRequest(new VotingPluginWire.SessionFenceRequest(
|
||||
request.player(), request.nonce(), request.server(), "Unknown")));
|
||||
assertEquals(1, outgoing.size(), "unknown origin must not receive a fence");
|
||||
receive.invoke(handler, outgoing.get(0));
|
||||
assertEquals(0, backendReplies.size(), "reply for another proxy must not be relayed");
|
||||
var local = new VotingPluginWire.SessionFenceRequest(request.player(), request.nonce(), request.server(), "Receiver");
|
||||
receive.invoke(handler, VotingPluginWire.sessionFenceReply(local, reply.fence()));
|
||||
assertEquals(1, backendReplies.size());
|
||||
assertEquals(local, VotingPluginWire.readSessionFenceReply(backendReplies.get(0)).request());
|
||||
}
|
||||
|
||||
private void assertReceiverCooldownTime(long senderTime) throws Exception {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
UUID voteId = UUID.randomUUID();
|
||||
VotingPluginProxyTestImpl proxy = spy(new VotingPluginProxyTestImpl());
|
||||
proxy.setDataFolder(temporaryDirectory.toFile());
|
||||
VoteCacheHandler voteCache = mock(VoteCacheHandler.class);
|
||||
when(voteCache.markMultiProxyVoteCompletedDurably(any())).thenReturn(true);
|
||||
when(voteCache.getTimeChangeQueue()).thenReturn(new ConcurrentLinkedQueue<>());
|
||||
doReturn(voteCache).when(proxy).getVoteCacheHandler();
|
||||
doNothing().when(proxy).addVoteParty();
|
||||
proxy.setMethod(BungeeMethod.PLUGINMESSAGING);
|
||||
proxy.setGlobalMessageProxyHandlerForTest(new GlobalMessageProxyHandler() {
|
||||
@Override public void sendMessage(String server, int delay, JsonEnvelope envelope) { }
|
||||
});
|
||||
|
||||
// Install the production callback while transport startup is disabled. Only
|
||||
// the transport acknowledgement is stubbed; decoding and vote production run.
|
||||
proxy.loadMultiProxySupport();
|
||||
MultiProxyHandler handler = spy(proxy.getMultiProxyHandler());
|
||||
doNothing().when(handler).acknowledgeMultiProxyVote(any(), anyString());
|
||||
proxy.setMultiProxyHandler(handler);
|
||||
when(proxy.getConfig().getMultiProxySupport()).thenReturn(true);
|
||||
when(proxy.getConfig().getPrimaryServer()).thenReturn(false);
|
||||
when(proxy.getConfig().getOnlineMode()).thenReturn(true);
|
||||
when(proxy.getConfig().getBungeeManageTotals()).thenReturn(true);
|
||||
when(proxy.getConfig().getSendVotesToAllServers()).thenReturn(false);
|
||||
Field legacyServers = VotingPluginProxy.class.getDeclaredField("legacyVoteDeliveryServers");
|
||||
legacyServers.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.Set<String> legacy = (java.util.Set<String>) legacyServers.get(proxy);
|
||||
legacy.add("server1");
|
||||
legacy.add("server2");
|
||||
|
||||
Method handleEnvelope = MultiProxyHandler.class.getDeclaredMethod("handleEnvelope", JsonEnvelope.class);
|
||||
handleEnvelope.setAccessible(true);
|
||||
long beforeReceive = System.currentTimeMillis();
|
||||
handleEnvelope.invoke(handler, VotingPluginWire.multiProxyVote("Player", playerId.toString(),
|
||||
"Service", senderTime, true, true, "", voteId, false, false, 1, 1, "Primary", true));
|
||||
long afterReceive = System.currentTimeMillis();
|
||||
|
||||
JsonEnvelope emitted = proxy.getLastVoteEnvelope();
|
||||
assertNotNull(emitted, "the actual receiver must produce a backend vote");
|
||||
VotingPluginWire.Vote forwarded = VotingPluginWire.readVote(emitted);
|
||||
assertTrue(forwarded.time >= beforeReceive && forwarded.time <= afterReceive,
|
||||
"ordinary backend LastVotes time must use the receiver clock");
|
||||
assertEquals(voteId, forwarded.voteId);
|
||||
assertEquals(playerId.toString(), forwarded.uuid);
|
||||
assertTrue(forwarded.delayValidationKnown);
|
||||
assertTrue(forwarded.delayValidated);
|
||||
assertTrue(forwarded.queuedDeliveryKnown);
|
||||
|
||||
// Exercise the real backend router and event adapter. No guide manager is
|
||||
// installed: provenance must not change ordinary vote/cooldown accounting.
|
||||
VotingPluginMain plugin = mock(VotingPluginMain.class);
|
||||
UserManager users = mock(UserManager.class);
|
||||
VotingPluginUser user = mock(VotingPluginUser.class);
|
||||
Field userPlugin = VotingPluginUser.class.getDeclaredField("plugin");
|
||||
userPlugin.setAccessible(true);
|
||||
userPlugin.set(user, plugin);
|
||||
when(user.getPlayerName()).thenReturn("Player");
|
||||
doCallRealMethod().when(user).bungeeVotePluginMessagingEvent(anyString(), anyLong(),
|
||||
any(VoteTotalsSnapshot.class), anyBoolean(), anyBoolean(), anyBoolean(), anyInt(),
|
||||
anyBoolean(), anyBoolean(), anyBoolean(), any(UUID.class), anyBoolean(), anyBoolean(), anyBoolean());
|
||||
BungeeSettings settings = mock(BungeeSettings.class);
|
||||
when(settings.isUseBungeecoord()).thenReturn(true);
|
||||
when(plugin.getBungeeSettings()).thenReturn(settings);
|
||||
when(plugin.getVotingPluginUserManager()).thenReturn(users);
|
||||
when(users.getVotingPluginUser(playerId, "Player")).thenReturn(user);
|
||||
ServerData serverData = mock(ServerData.class);
|
||||
when(plugin.getServerData()).thenReturn(serverData);
|
||||
VoteSiteManager sites = mock(VoteSiteManager.class);
|
||||
VoteSite site = mock(VoteSite.class);
|
||||
when(plugin.getVoteSiteManager()).thenReturn(sites);
|
||||
when(sites.getVoteSite("Service", true)).thenReturn(site);
|
||||
Server server = mock(Server.class);
|
||||
PluginManager events = mock(PluginManager.class);
|
||||
when(plugin.getServer()).thenReturn(server);
|
||||
when(server.getPluginManager()).thenReturn(events);
|
||||
ProcessedVoteCache processed = mock(ProcessedVoteCache.class);
|
||||
when(processed.reserveWithOutcome(voteId)).thenReturn(ProcessedVoteCache.Reservation.RESERVED);
|
||||
when(processed.complete(voteId)).thenReturn(true);
|
||||
BackendProxyMessageRouter router = new BackendProxyMessageRouter(plugin,
|
||||
mock(BackendPresenceManager.class), mock(BackendGlobalDataSync.class),
|
||||
mock(BackendVotePartySync.class), processed);
|
||||
// Check completion afterwards so a fixture error retains its original
|
||||
// exception instead of being replaced by the quarantine callback assertion.
|
||||
AtomicReference<BackendProxyMessageRouter.OrderedVoteOutcome> outcome = new AtomicReference<>();
|
||||
router.handleOrderedVote(emitted, outcome::set);
|
||||
assertEquals(BackendProxyMessageRouter.OrderedVoteOutcome.COMPLETE, outcome.get());
|
||||
ArgumentCaptor<PlayerVoteEvent> event = ArgumentCaptor.forClass(PlayerVoteEvent.class);
|
||||
verify(events).callEvent(event.capture());
|
||||
assertEquals(forwarded.time, event.getValue().getTime());
|
||||
assertEquals(voteId, event.getValue().getProxyVoteId());
|
||||
assertEquals(!forwarded.manageTotals, event.getValue().isAddTotals());
|
||||
assertTrue(event.getValue().isUnconfirmedProxySessionDelivery());
|
||||
assertTrue(event.getValue().isProxyDelayValidationKnown());
|
||||
assertTrue(event.getValue().isProxyQueueClassificationKnown());
|
||||
assertEquals(forwarded.queuedDelivery, event.getValue().isQueuedProxyVote());
|
||||
|
||||
assertNormalAccountingTime(event.getValue(), site, user);
|
||||
}
|
||||
|
||||
private void assertNormalAccountingTime(PlayerVoteEvent event, VoteSite site, VotingPluginUser user) {
|
||||
@SuppressWarnings("unchecked")
|
||||
SharedVoteProcessor.Operations<VoteSite, VotingPluginUser> operations = mock(SharedVoteProcessor.Operations.class);
|
||||
when(operations.enabled()).thenReturn(true);
|
||||
when(operations.incomingName()).thenReturn("Player");
|
||||
when(operations.properName("Player")).thenReturn("Player");
|
||||
when(operations.validate("Player", false)).thenReturn(
|
||||
new SharedVoteProcessor.Validation(true, "Player", "CACHE", "ok", false));
|
||||
when(operations.resolveSite()).thenReturn(site);
|
||||
when(operations.siteEnabled(site)).thenReturn(true);
|
||||
when(operations.resolveUser("Player")).thenReturn(user);
|
||||
when(operations.proxyVote()).thenReturn(event.isBungee());
|
||||
when(operations.forceProxyRouting()).thenReturn(event.isForceBungee());
|
||||
when(operations.wasOnline()).thenReturn(event.isWasOnline());
|
||||
when(operations.realVote()).thenReturn(event.isRealVote());
|
||||
when(operations.addTotals()).thenReturn(event.isAddTotals());
|
||||
when(operations.proxyVoteId()).thenReturn(event.getProxyVoteId());
|
||||
when(operations.proxyQueueClassificationKnown()).thenReturn(event.isProxyQueueClassificationKnown());
|
||||
when(operations.proxyDelayValidationKnown()).thenReturn(event.isProxyDelayValidationKnown());
|
||||
when(operations.incomingTime()).thenReturn(event.getTime());
|
||||
when(operations.countingPolicy()).thenReturn(new SharedVotePolicy(false, true, false, false, false));
|
||||
SharedVoteProcessor.process(operations);
|
||||
verify(operations).setTime(user, site, event.getTime());
|
||||
verify(operations).cooldown(user, site);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
package com.bencodez.votingplugin.tests;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@@ -63,7 +65,7 @@ class ProxyCacheCheckedReaderJdbcTest {
|
||||
}
|
||||
Field mysqlField = AbstractSqlTable.class.getDeclaredField("mysql");
|
||||
mysqlField.setAccessible(true);
|
||||
mysqlField.set(table, mysql);
|
||||
ReflectiveAccess.set(mysqlField, table, mysql);
|
||||
return new Fixture(checked, legacy, connection, statement, result);
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -1,4 +1,7 @@
|
||||
package com.bencodez.votingplugin;
|
||||
package com.bencodez.votingplugin.tests;
|
||||
|
||||
import com.bencodez.votingplugin.VotingPluginHooks;
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.bencodez.votingplugin.tests;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.*;
|
||||
import com.bencodez.votingplugin.VotingPluginMain.*;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Player;
|
||||
import com.bencodez.votingplugin.backendproxy.BackendProxyHandler;
|
||||
|
||||
/** Typed test-only access to package-private VotingPluginMain seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class VotingPluginMainAccess {
|
||||
private VotingPluginMainAccess() { }
|
||||
public static void closePublishedPreviousBackendProxyHandler(VotingPluginMain target, BackendProxyHandler previous, String phase) { ReflectiveAccess.call("com.bencodez.votingplugin.VotingPluginMain", target, "closePublishedPreviousBackendProxyHandler", new String[]{"BackendProxyHandler", "String"}, new Object[]{previous, phase}); }
|
||||
public static void publishBackendProxyHandler(VotingPluginMain target, BackendProxyHandler previous, BackendProxyHandler replacement) { ReflectiveAccess.call("com.bencodez.votingplugin.VotingPluginMain", target, "publishBackendProxyHandler", new String[]{"BackendProxyHandler", "BackendProxyHandler"}, new Object[]{previous, replacement}); }
|
||||
public static UUID placeholderStorageUuid(Player player, boolean onlineMode) { return (UUID) ReflectiveAccess.call("com.bencodez.votingplugin.VotingPluginMain", null, "placeholderStorageUuid", new String[]{"Player", "boolean"}, new Object[]{player, onlineMode}); }
|
||||
public static void reloadBackendProxyRuntime(VotingPluginMain target, boolean updateActiveRuntime, boolean userStorage) { ReflectiveAccess.call("com.bencodez.votingplugin.VotingPluginMain", target, "reloadBackendProxyRuntime", new String[]{"boolean", "boolean"}, new Object[]{updateActiveRuntime, userStorage}); }
|
||||
}
|
||||
+16
-12
@@ -1,4 +1,8 @@
|
||||
package com.bencodez.votingplugin;
|
||||
package com.bencodez.votingplugin.tests;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -44,7 +48,7 @@ class VotingPluginMainBackendProxyPublicationTest {
|
||||
setField(plugin, "bungeeSettings", settings);
|
||||
when(settings.isUseBungeecoord()).thenReturn(true);
|
||||
|
||||
plugin.reloadBackendProxyRuntime(true, false);
|
||||
VotingPluginMainAccess.reloadBackendProxyRuntime(plugin, true, false);
|
||||
|
||||
verify(active).reloadSharedTransportSecurity();
|
||||
verify(active).reloadPresenceReporting();
|
||||
@@ -63,7 +67,7 @@ class VotingPluginMainBackendProxyPublicationTest {
|
||||
|
||||
// Full-editor Control preparation reloads settings before this replacement
|
||||
// can be validated or published. It must not reconfigure the live handler.
|
||||
plugin.reloadBackendProxyRuntime(false, false);
|
||||
VotingPluginMainAccess.reloadBackendProxyRuntime(plugin, false, false);
|
||||
VotingPluginMain.BackendProxyRestart restart = restart(previous, replacement);
|
||||
assertThrows(IllegalStateException.class, () -> plugin.completeBackendProxyHandlerRestart(restart));
|
||||
|
||||
@@ -79,7 +83,7 @@ class VotingPluginMainBackendProxyPublicationTest {
|
||||
setField(plugin, "bungeeSettings", settings);
|
||||
when(settings.isUseBungeecoord()).thenReturn(true);
|
||||
|
||||
plugin.reloadBackendProxyRuntime(false, false);
|
||||
VotingPluginMainAccess.reloadBackendProxyRuntime(plugin, false, false);
|
||||
|
||||
assertNull(plugin.getBackendProxyHandler());
|
||||
}
|
||||
@@ -96,7 +100,7 @@ class VotingPluginMainBackendProxyPublicationTest {
|
||||
Method load = VotingPluginMain.class.getDeclaredMethod("loadBungeeHandler");
|
||||
load.setAccessible(true);
|
||||
|
||||
assertDoesNotThrow(() -> load.invoke(plugin));
|
||||
assertDoesNotThrow(() -> ReflectiveAccess.invoke(load, plugin, new Object[]{}));
|
||||
assertNull(plugin.getBackendProxyHandler());
|
||||
}
|
||||
|
||||
@@ -157,7 +161,7 @@ class VotingPluginMainBackendProxyPublicationTest {
|
||||
return null;
|
||||
}).when(replacement).activatePresenceReporting();
|
||||
|
||||
plugin.publishBackendProxyHandler(null, replacement);
|
||||
VotingPluginMainAccess.publishBackendProxyHandler(plugin, null, replacement);
|
||||
|
||||
assertSame(replacement, plugin.getBackendProxyHandler());
|
||||
}
|
||||
@@ -171,7 +175,7 @@ class VotingPluginMainBackendProxyPublicationTest {
|
||||
.when(replacement).activatePresenceReporting();
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException.class,
|
||||
() -> plugin.publishBackendProxyHandler(previous, replacement));
|
||||
() -> VotingPluginMainAccess.publishBackendProxyHandler(plugin, previous, replacement));
|
||||
|
||||
assertSame(previous, plugin.getBackendProxyHandler());
|
||||
verify(replacement).abortStagedInboundTo(previous);
|
||||
@@ -268,7 +272,7 @@ class VotingPluginMainBackendProxyPublicationTest {
|
||||
org.mockito.ArgumentMatchers.anyLong());
|
||||
verify(previous, never()).close();
|
||||
|
||||
plugin.closePublishedPreviousBackendProxyHandler(previous, "after publication");
|
||||
VotingPluginMainAccess.closePublishedPreviousBackendProxyHandler(plugin, previous, "after publication");
|
||||
assertTrue(closed.await(1, TimeUnit.SECONDS));
|
||||
assertTrue(closeThread.get().startsWith("VotingPlugin-Retired-Backend"));
|
||||
}
|
||||
@@ -332,7 +336,7 @@ class VotingPluginMainBackendProxyPublicationTest {
|
||||
|
||||
try {
|
||||
org.junit.jupiter.api.Assertions.assertTimeoutPreemptively(java.time.Duration.ofMillis(250),
|
||||
() -> plugin.closePublishedPreviousBackendProxyHandler(previous, "after publication"));
|
||||
() -> VotingPluginMainAccess.closePublishedPreviousBackendProxyHandler(plugin, previous, "after publication"));
|
||||
assertTrue(closeStarted.await(1, TimeUnit.SECONDS));
|
||||
assertTrue(closeThread.get().startsWith("VotingPlugin-Retired-Backend"));
|
||||
} finally {
|
||||
@@ -705,7 +709,7 @@ class VotingPluginMainBackendProxyPublicationTest {
|
||||
Constructor<VotingPluginMain.BackendProxyRestart> constructor = VotingPluginMain.BackendProxyRestart.class
|
||||
.getDeclaredConstructor(BackendProxyHandler.class, BackendProxyHandler.class, boolean.class, boolean.class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(previous, replacement, replacement == null, previousRequiresPreparation);
|
||||
return ReflectiveAccess.newInstance(constructor, new Object[]{previous, replacement, replacement == null, previousRequiresPreparation});
|
||||
}
|
||||
|
||||
private void setBackendProxyHandler(VotingPluginMain plugin, BackendProxyHandler handler) throws Exception {
|
||||
@@ -715,13 +719,13 @@ class VotingPluginMainBackendProxyPublicationTest {
|
||||
private void setField(VotingPluginMain plugin, String name, Object value) throws Exception {
|
||||
Field field = VotingPluginMain.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(plugin, value);
|
||||
ReflectiveAccess.set(field, plugin, value);
|
||||
}
|
||||
|
||||
private void setRestartField(VotingPluginMain.BackendProxyRestart restart, String name, Object value)
|
||||
throws Exception {
|
||||
Field field = VotingPluginMain.BackendProxyRestart.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(restart, value);
|
||||
ReflectiveAccess.set(field, restart, value);
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -1,4 +1,6 @@
|
||||
package com.bencodez.votingplugin;
|
||||
package com.bencodez.votingplugin.tests;
|
||||
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.*;
|
||||
@@ -145,18 +147,18 @@ class VotingPluginMainPlaceholderPresenceTest {
|
||||
verify(plugin, times(2)).captureOnlineTopVoterIgnore(any(), any());
|
||||
}
|
||||
@Test
|
||||
void cachedStorageIdentityWinsDuringColdPresenceRefresh() {
|
||||
void offlineStorageIdentityWinsDuringColdPresenceRefresh() {
|
||||
Player player = mock(Player.class);
|
||||
UuidLookup lookup = mock(UuidLookup.class);
|
||||
UUID bukkitUuid = UUID.randomUUID();
|
||||
UUID storageUuid = UUID.randomUUID();
|
||||
when(player.getName()).thenReturn("Player");
|
||||
when(player.getUniqueId()).thenReturn(bukkitUuid);
|
||||
when(lookup.getCachedUUID("Player")).thenReturn(storageUuid.toString());
|
||||
when(lookup.getUUIDWithoutStorage("Player")).thenReturn(storageUuid.toString());
|
||||
|
||||
try (var uuidLookup = mockStatic(UuidLookup.class)) {
|
||||
uuidLookup.when(UuidLookup::getInstance).thenReturn(lookup);
|
||||
assertEquals(storageUuid, VotingPluginMain.placeholderStorageUuid(player, false));
|
||||
assertEquals(storageUuid, VotingPluginMainAccess.placeholderStorageUuid(player, false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,11 +169,11 @@ class VotingPluginMainPlaceholderPresenceTest {
|
||||
UUID bukkitUuid = UUID.randomUUID();
|
||||
when(player.getName()).thenReturn("Player");
|
||||
when(player.getUniqueId()).thenReturn(bukkitUuid);
|
||||
when(lookup.getCachedUUID("Player")).thenReturn("");
|
||||
when(lookup.getUUIDWithoutStorage("Player")).thenReturn("");
|
||||
|
||||
try (var uuidLookup = mockStatic(UuidLookup.class)) {
|
||||
uuidLookup.when(UuidLookup::getInstance).thenReturn(lookup);
|
||||
assertEquals(bukkitUuid, VotingPluginMain.placeholderStorageUuid(player, false));
|
||||
assertEquals(bukkitUuid, VotingPluginMainAccess.placeholderStorageUuid(player, false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +186,7 @@ class VotingPluginMainPlaceholderPresenceTest {
|
||||
|
||||
try (var uuidLookup = mockStatic(UuidLookup.class)) {
|
||||
uuidLookup.when(UuidLookup::getInstance).thenReturn(lookup);
|
||||
assertEquals(bukkitUuid, VotingPluginMain.placeholderStorageUuid(player, true));
|
||||
assertEquals(bukkitUuid, VotingPluginMainAccess.placeholderStorageUuid(player, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
-73
@@ -1,6 +1,8 @@
|
||||
|
||||
package com.bencodez.votingplugin.tests;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
@@ -245,7 +247,7 @@ public class VotingPluginProxyTest {
|
||||
com.bencodez.votingplugin.proxy.security.TransportEnvelopeEncryption.Domain.PROXY_BACKEND, true);
|
||||
var field = VotingPluginProxy.class.getDeclaredField("communicationEncryption");
|
||||
field.setAccessible(true);
|
||||
field.set(votingPluginProxy, encryption);
|
||||
ReflectiveAccess.set(field, votingPluginProxy, encryption);
|
||||
return encryption;
|
||||
}
|
||||
|
||||
@@ -384,7 +386,7 @@ public class VotingPluginProxyTest {
|
||||
Mockito.mock(com.bencodez.simpleapi.servercomm.global.GlobalMessageProxyHandler.class));
|
||||
java.lang.reflect.Field decider = VotingPluginProxy.class.getDeclaredField("proxyBroadcastDecider");
|
||||
decider.setAccessible(true);
|
||||
decider.set(votingPluginProxy, new ProxyBroadcastDecider(votingPluginProxy::getConfig,
|
||||
ReflectiveAccess.set(decider, votingPluginProxy, new ProxyBroadcastDecider(votingPluginProxy::getConfig,
|
||||
votingPluginProxy::getAllAvailableServers, server -> true, server -> false));
|
||||
java.util.UUID voteId = java.util.UUID.randomUUID();
|
||||
|
||||
@@ -711,7 +713,7 @@ public class VotingPluginProxyTest {
|
||||
Class<?> retryType = Class.forName("com.bencodez.votingplugin.proxy.VotingPluginProxy$LiveVoteRetryState");
|
||||
java.lang.reflect.Constructor<?> constructor = retryType.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Object retry = constructor.newInstance();
|
||||
Object retry = ReflectiveAccess.newInstance(constructor, new Object[]{});
|
||||
for (java.util.Map.Entry<String, Object> field : java.util.Map.<String, Object>of(
|
||||
"player", "Player", "service", "Service", "uuid",
|
||||
"00000000-0000-0000-0000-000000000001", "time", 100L, "realVote", true,
|
||||
@@ -730,11 +732,9 @@ public class VotingPluginProxyTest {
|
||||
retryType, VoteTimeQueue.class, String.class, String.class, String.class, long.class,
|
||||
boolean.class, com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class);
|
||||
begin.setAccessible(true);
|
||||
assertFalse((Boolean) begin.invoke(spyProxy, retry, null, "Player",
|
||||
"00000000-0000-0000-0000-000000000001", "Service", 100L, true, null));
|
||||
assertFalse((Boolean) ReflectiveAccess.invoke(begin, spyProxy, new Object[]{retry, null, "Player", "00000000-0000-0000-0000-000000000001", "Service", 100L, true, null}));
|
||||
verify(multiProxyHandler, never()).sendMultiProxyEnvelopeAccepted(Mockito.any(), Mockito.any());
|
||||
assertFalse((Boolean) begin.invoke(spyProxy, retry, null, "Player",
|
||||
"00000000-0000-0000-0000-000000000001", "Service", 100L, true, null));
|
||||
assertFalse((Boolean) ReflectiveAccess.invoke(begin, spyProxy, new Object[]{retry, null, "Player", "00000000-0000-0000-0000-000000000001", "Service", 100L, true, null}));
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
org.mockito.ArgumentCaptor<java.util.Collection<String>> recipients =
|
||||
(org.mockito.ArgumentCaptor) org.mockito.ArgumentCaptor.forClass(java.util.Collection.class);
|
||||
@@ -744,7 +744,7 @@ public class VotingPluginProxyTest {
|
||||
java.lang.reflect.Method settle = VotingPluginProxy.class
|
||||
.getDeclaredMethod("settleLiveVoteRetriesForFinalShutdown");
|
||||
settle.setAccessible(true);
|
||||
assertFalse((Boolean) settle.invoke(spyProxy));
|
||||
assertFalse((Boolean) ReflectiveAccess.invoke(settle, spyProxy, new Object[]{}));
|
||||
|
||||
org.mockito.ArgumentCaptor<VoteTimeQueue> outboxes = org.mockito.ArgumentCaptor.forClass(VoteTimeQueue.class);
|
||||
verify(voteCache, Mockito.atLeast(2)).addTimeVoteToCache(outboxes.capture());
|
||||
@@ -767,7 +767,7 @@ public class VotingPluginProxyTest {
|
||||
Class<?> retryType = Class.forName("com.bencodez.votingplugin.proxy.VotingPluginProxy$LiveVoteRetryState");
|
||||
java.lang.reflect.Constructor<?> constructor = retryType.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Object retry = constructor.newInstance();
|
||||
Object retry = ReflectiveAccess.newInstance(constructor, new Object[]{});
|
||||
java.util.UUID voteId = java.util.UUID.randomUUID();
|
||||
java.lang.reflect.Field retriesField = VotingPluginProxy.class.getDeclaredField("liveVoteRetries");
|
||||
retriesField.setAccessible(true);
|
||||
@@ -780,8 +780,7 @@ public class VotingPluginProxyTest {
|
||||
boolean.class, com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class);
|
||||
begin.setAccessible(true);
|
||||
|
||||
assertFalse((Boolean) begin.invoke(votingPluginProxy, retry, null, "Player",
|
||||
"00000000-0000-0000-0000-000000000001", "Service", 100L, true, null));
|
||||
assertFalse((Boolean) ReflectiveAccess.invoke(begin, votingPluginProxy, new Object[]{retry, null, "Player", "00000000-0000-0000-0000-000000000001", "Service", 100L, true, null}));
|
||||
org.mockito.ArgumentCaptor<JsonEnvelope> envelope = org.mockito.ArgumentCaptor.forClass(JsonEnvelope.class);
|
||||
verify(multiProxyHandler).sendMultiProxyEnvelopeAccepted(envelope.capture(),
|
||||
Mockito.eq(java.util.Set.of("ProxyLegacy")));
|
||||
@@ -822,7 +821,7 @@ public class VotingPluginProxyTest {
|
||||
Class<?> retryType = Class.forName("com.bencodez.votingplugin.proxy.VotingPluginProxy$LiveVoteRetryState");
|
||||
java.lang.reflect.Constructor<?> constructor = retryType.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Object retry = constructor.newInstance();
|
||||
Object retry = ReflectiveAccess.newInstance(constructor, new Object[]{});
|
||||
java.util.UUID voteId = java.util.UUID.randomUUID();
|
||||
java.lang.reflect.Field retriesField = VotingPluginProxy.class.getDeclaredField("liveVoteRetries");
|
||||
retriesField.setAccessible(true);
|
||||
@@ -835,8 +834,7 @@ public class VotingPluginProxyTest {
|
||||
boolean.class, com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class);
|
||||
begin.setAccessible(true);
|
||||
|
||||
assertFalse((Boolean) begin.invoke(proxy, retry, null, "Player",
|
||||
"00000000-0000-0000-0000-000000000001", "Service", 100L, true, null));
|
||||
assertFalse((Boolean) ReflectiveAccess.invoke(begin, proxy, new Object[]{retry, null, "Player", "00000000-0000-0000-0000-000000000001", "Service", 100L, true, null}));
|
||||
verify(multiProxyHandler, never()).sendMultiProxyEnvelopeAccepted(Mockito.any(), Mockito.any());
|
||||
assertEquals(1, queue.size());
|
||||
assertTrue(queue.element().isMultiProxyCapabilityDiscoveryPending());
|
||||
@@ -844,15 +842,13 @@ public class VotingPluginProxyTest {
|
||||
verify(scheduler).schedule(Mockito.any(Runnable.class), Mockito.eq(5L),
|
||||
Mockito.eq(java.util.concurrent.TimeUnit.SECONDS));
|
||||
|
||||
assertTrue((Boolean) begin.invoke(proxy, retry, queue.element(), "Player",
|
||||
"00000000-0000-0000-0000-000000000001", "Service", 100L, true, null));
|
||||
assertTrue((Boolean) ReflectiveAccess.invoke(begin, proxy, new Object[]{retry, queue.element(), "Player", "00000000-0000-0000-0000-000000000001", "Service", 100L, true, null}));
|
||||
verify(multiProxyHandler).sendMultiProxyEnvelopeAccepted(Mockito.any(), Mockito.eq(java.util.Set.of("Candidate")));
|
||||
assertFalse(queue.element().isMultiProxyCapabilityDiscoveryPending());
|
||||
assertTrue(queue.element().isMultiProxyForwardingHandled(),
|
||||
"the durable discovery fence must be retired before reporting the legacy fallback complete");
|
||||
assertTrue(queue.element().getMultiProxyLegacyPendingRecipients().isEmpty());
|
||||
assertTrue((Boolean) begin.invoke(proxy, retry, queue.element(), "Player",
|
||||
"00000000-0000-0000-0000-000000000001", "Service", 100L, true, null));
|
||||
assertTrue((Boolean) ReflectiveAccess.invoke(begin, proxy, new Object[]{retry, queue.element(), "Player", "00000000-0000-0000-0000-000000000001", "Service", 100L, true, null}));
|
||||
verify(multiProxyHandler, Mockito.times(1)).sendMultiProxyEnvelopeAccepted(Mockito.any(),
|
||||
Mockito.eq(java.util.Set.of("Candidate")));
|
||||
verify(multiProxyHandler).renewMultiProxyVoteCapabilityDiscoveryIfDue();
|
||||
@@ -872,14 +868,13 @@ public class VotingPluginProxyTest {
|
||||
Class<?> retryType = Class.forName("com.bencodez.votingplugin.proxy.VotingPluginProxy$LiveVoteRetryState");
|
||||
java.lang.reflect.Constructor<?> constructor = retryType.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Object retry = constructor.newInstance();
|
||||
Object retry = ReflectiveAccess.newInstance(constructor, new Object[]{});
|
||||
java.lang.reflect.Method begin = VotingPluginProxy.class.getDeclaredMethod("beginMultiProxyForwarding",
|
||||
retryType, VoteTimeQueue.class, String.class, String.class, String.class, long.class,
|
||||
boolean.class, com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class);
|
||||
begin.setAccessible(true);
|
||||
|
||||
assertFalse((Boolean) begin.invoke(votingPluginProxy, retry, null, "Player",
|
||||
"00000000-0000-0000-0000-000000000001", "Service", 100L, true, null));
|
||||
assertFalse((Boolean) ReflectiveAccess.invoke(begin, votingPluginProxy, new Object[]{retry, null, "Player", "00000000-0000-0000-0000-000000000001", "Service", 100L, true, null}));
|
||||
verify(multiProxyHandler, never()).sendMultiProxyEnvelopeAccepted(Mockito.any(), Mockito.any());
|
||||
}
|
||||
|
||||
@@ -893,14 +888,13 @@ public class VotingPluginProxyTest {
|
||||
Class<?> retryType = Class.forName("com.bencodez.votingplugin.proxy.VotingPluginProxy$LiveVoteRetryState");
|
||||
java.lang.reflect.Constructor<?> constructor = retryType.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Object retry = constructor.newInstance();
|
||||
Object retry = ReflectiveAccess.newInstance(constructor, new Object[]{});
|
||||
java.lang.reflect.Method begin = VotingPluginProxy.class.getDeclaredMethod("beginMultiProxyForwarding",
|
||||
retryType, VoteTimeQueue.class, String.class, String.class, String.class, long.class,
|
||||
boolean.class, com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class);
|
||||
begin.setAccessible(true);
|
||||
|
||||
assertFalse((Boolean) begin.invoke(spyProxy, retry, null, "Player",
|
||||
"00000000-0000-0000-0000-000000000001", "Service", 100L, true, null));
|
||||
assertFalse((Boolean) ReflectiveAccess.invoke(begin, spyProxy, new Object[]{retry, null, "Player", "00000000-0000-0000-0000-000000000001", "Service", 100L, true, null}));
|
||||
verify(multiProxyHandler, never()).announceMultiProxyVoteCapability();
|
||||
verify(multiProxyHandler, never()).sendMultiProxyEnvelopeAccepted(Mockito.any(), Mockito.any());
|
||||
verify(voteCache, never()).addTimeVoteToCache(Mockito.any());
|
||||
@@ -928,7 +922,7 @@ public class VotingPluginProxyTest {
|
||||
Class<?> retryType = Class.forName("com.bencodez.votingplugin.proxy.VotingPluginProxy$LiveVoteRetryState");
|
||||
java.lang.reflect.Constructor<?> constructor = retryType.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Object retry = constructor.newInstance();
|
||||
Object retry = ReflectiveAccess.newInstance(constructor, new Object[]{});
|
||||
java.util.UUID voteId = java.util.UUID.randomUUID();
|
||||
java.lang.reflect.Field retriesField = VotingPluginProxy.class.getDeclaredField("liveVoteRetries");
|
||||
retriesField.setAccessible(true);
|
||||
@@ -941,8 +935,7 @@ public class VotingPluginProxyTest {
|
||||
boolean.class, com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class);
|
||||
begin.setAccessible(true);
|
||||
|
||||
assertFalse((Boolean) begin.invoke(spyProxy, retry, null, "Player",
|
||||
"00000000-0000-0000-0000-000000000001", "Service", 100L, true, null));
|
||||
assertFalse((Boolean) ReflectiveAccess.invoke(begin, spyProxy, new Object[]{retry, null, "Player", "00000000-0000-0000-0000-000000000001", "Service", 100L, true, null}));
|
||||
assertEquals(1, queue.size());
|
||||
assertEquals(java.util.Set.of("capable"), queue.element().getMultiProxyRecipients());
|
||||
assertTrue(queue.element().getMultiProxyLegacyPendingRecipients().isEmpty());
|
||||
@@ -975,7 +968,7 @@ public class VotingPluginProxyTest {
|
||||
Class<?> retryType = Class.forName("com.bencodez.votingplugin.proxy.VotingPluginProxy$LiveVoteRetryState");
|
||||
java.lang.reflect.Constructor<?> constructor = retryType.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Object retry = constructor.newInstance();
|
||||
Object retry = ReflectiveAccess.newInstance(constructor, new Object[]{});
|
||||
java.util.UUID voteId = java.util.UUID.randomUUID();
|
||||
java.lang.reflect.Field retriesField = VotingPluginProxy.class.getDeclaredField("liveVoteRetries");
|
||||
retriesField.setAccessible(true);
|
||||
@@ -988,8 +981,7 @@ public class VotingPluginProxyTest {
|
||||
boolean.class, com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class);
|
||||
begin.setAccessible(true);
|
||||
|
||||
assertFalse((Boolean) begin.invoke(proxy, retry, null, "Player",
|
||||
"00000000-0000-0000-0000-000000000001", "Service", 100L, true, null));
|
||||
assertFalse((Boolean) ReflectiveAccess.invoke(begin, proxy, new Object[]{retry, null, "Player", "00000000-0000-0000-0000-000000000001", "Service", 100L, true, null}));
|
||||
assertEquals(1, queue.size());
|
||||
verify(scheduler).schedule(Mockito.any(Runnable.class), Mockito.eq(5L),
|
||||
Mockito.eq(java.util.concurrent.TimeUnit.SECONDS));
|
||||
@@ -1096,7 +1088,7 @@ public class VotingPluginProxyTest {
|
||||
Class<?> retryType = Class.forName("com.bencodez.votingplugin.proxy.VotingPluginProxy$LiveVoteRetryState");
|
||||
java.lang.reflect.Constructor<?> constructor = retryType.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Object retry = constructor.newInstance();
|
||||
Object retry = ReflectiveAccess.newInstance(constructor, new Object[]{});
|
||||
java.lang.reflect.Field retriesField = VotingPluginProxy.class.getDeclaredField("liveVoteRetries");
|
||||
retriesField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -1176,17 +1168,17 @@ public class VotingPluginProxyTest {
|
||||
Class<?> retryType = Class.forName("com.bencodez.votingplugin.proxy.VotingPluginProxy$LiveVoteRetryState");
|
||||
java.lang.reflect.Constructor<?> retryConstructor = retryType.getDeclaredConstructor();
|
||||
retryConstructor.setAccessible(true);
|
||||
Object retry = retryConstructor.newInstance();
|
||||
Object retry = ReflectiveAccess.newInstance(retryConstructor, new Object[]{});
|
||||
java.lang.reflect.Method finish = VotingPluginProxy.class.getDeclaredMethod("finishMultiProxyRetirement",
|
||||
retryType, VoteTimeQueue.class);
|
||||
finish.setAccessible(true);
|
||||
|
||||
assertFalse((Boolean) finish.invoke(spyProxy, retry, outbox));
|
||||
assertFalse((Boolean) ReflectiveAccess.invoke(finish, spyProxy, new Object[]{retry, outbox}));
|
||||
assertFalse(outbox.isMultiProxyForwardingHandled());
|
||||
verify(multiProxyHandler).requestMultiProxyVoteRetirement(voteId, "Proxy1", "proxy2");
|
||||
|
||||
assertTrue(outbox.acknowledgeMultiProxyRetirement("Proxy2"));
|
||||
assertTrue((Boolean) finish.invoke(spyProxy, retry, outbox));
|
||||
assertTrue((Boolean) ReflectiveAccess.invoke(finish, spyProxy, new Object[]{retry, outbox}));
|
||||
assertTrue(outbox.isMultiProxyForwardingHandled());
|
||||
verify(voteCache).updateTimeVote(outbox);
|
||||
}
|
||||
@@ -1234,11 +1226,11 @@ public class VotingPluginProxyTest {
|
||||
"handleMultiProxyVoteRetirementRequest", java.util.UUID.class, String.class);
|
||||
retire.setAccessible(true);
|
||||
|
||||
retire.invoke(spyProxy, voteId, "Primary");
|
||||
ReflectiveAccess.invoke(retire, spyProxy, new Object[]{voteId, "Primary"});
|
||||
verify(voteCache, never()).removeMultiProxyVoteCompletion(voteId);
|
||||
verify(multiProxyHandler, never()).acknowledgeMultiProxyVoteRetirement(voteId, "Primary");
|
||||
|
||||
retire.invoke(spyProxy, voteId, "Primary");
|
||||
ReflectiveAccess.invoke(retire, spyProxy, new Object[]{voteId, "Primary"});
|
||||
verify(voteCache).removeMultiProxyVoteCompletion(voteId);
|
||||
verify(multiProxyHandler).acknowledgeMultiProxyVoteRetirement(voteId, "Primary");
|
||||
}
|
||||
@@ -1271,7 +1263,7 @@ public class VotingPluginProxyTest {
|
||||
Mockito.doNothing().when(spyProxy).addVoteParty();
|
||||
java.lang.reflect.Field decider = VotingPluginProxy.class.getDeclaredField("proxyBroadcastDecider");
|
||||
decider.setAccessible(true);
|
||||
decider.set(spyProxy, new ProxyBroadcastDecider(spyProxy::getConfig, spyProxy::getAllAvailableServers,
|
||||
ReflectiveAccess.set(decider, spyProxy, new ProxyBroadcastDecider(spyProxy::getConfig, spyProxy::getAllAvailableServers,
|
||||
server -> true, server -> false));
|
||||
|
||||
spyProxy.processQueue();
|
||||
@@ -1320,7 +1312,7 @@ public class VotingPluginProxyTest {
|
||||
.thenReturn(new java.util.ArrayList<>());
|
||||
java.lang.reflect.Field decider = VotingPluginProxy.class.getDeclaredField("proxyBroadcastDecider");
|
||||
decider.setAccessible(true);
|
||||
decider.set(spyProxy, new ProxyBroadcastDecider(spyProxy::getConfig, spyProxy::getAllAvailableServers,
|
||||
ReflectiveAccess.set(decider, spyProxy, new ProxyBroadcastDecider(spyProxy::getConfig, spyProxy::getAllAvailableServers,
|
||||
server -> true, server -> false));
|
||||
|
||||
spyProxy.processQueue();
|
||||
@@ -1368,7 +1360,7 @@ public class VotingPluginProxyTest {
|
||||
Mockito.when(proxyMySQL.getExactQuery(Mockito.any())).thenReturn(new java.util.ArrayList<>());
|
||||
java.lang.reflect.Field decider = VotingPluginProxy.class.getDeclaredField("proxyBroadcastDecider");
|
||||
decider.setAccessible(true);
|
||||
decider.set(spyProxy, new ProxyBroadcastDecider(spyProxy::getConfig, spyProxy::getAllAvailableServers,
|
||||
ReflectiveAccess.set(decider, spyProxy, new ProxyBroadcastDecider(spyProxy::getConfig, spyProxy::getAllAvailableServers,
|
||||
server -> true, server -> false));
|
||||
|
||||
spyProxy.processQueue();
|
||||
@@ -1445,7 +1437,7 @@ public class VotingPluginProxyTest {
|
||||
Mockito.doNothing().when(spyProxy).addVoteParty();
|
||||
java.lang.reflect.Field decider = VotingPluginProxy.class.getDeclaredField("proxyBroadcastDecider");
|
||||
decider.setAccessible(true);
|
||||
decider.set(spyProxy, new ProxyBroadcastDecider(spyProxy::getConfig, spyProxy::getAllAvailableServers,
|
||||
ReflectiveAccess.set(decider, spyProxy, new ProxyBroadcastDecider(spyProxy::getConfig, spyProxy::getAllAvailableServers,
|
||||
server -> true, server -> false));
|
||||
|
||||
spyProxy.processQueue();
|
||||
@@ -1465,7 +1457,7 @@ public class VotingPluginProxyTest {
|
||||
OfflineBungeeVote.class, boolean.class, boolean.class, int.class, int.class);
|
||||
envelopeMethod.setAccessible(true);
|
||||
|
||||
JsonEnvelope envelope = (JsonEnvelope) envelopeMethod.invoke(votingPluginProxy, cached, true, false, 1, 1);
|
||||
JsonEnvelope envelope = (JsonEnvelope) ReflectiveAccess.invoke(envelopeMethod, votingPluginProxy, new Object[]{cached, true, false, 1, 1});
|
||||
VotingPluginWire.Vote vote = VotingPluginWire.readVote(envelope);
|
||||
|
||||
assertTrue(vote.queuedDeliveryKnown);
|
||||
@@ -1483,8 +1475,7 @@ public class VotingPluginProxyTest {
|
||||
OfflineBungeeVote.class, boolean.class, boolean.class, int.class, int.class);
|
||||
envelopeMethod.setAccessible(true);
|
||||
|
||||
VotingPluginWire.Vote vote = VotingPluginWire.readVote((JsonEnvelope) envelopeMethod.invoke(
|
||||
votingPluginProxy, cached, true, false, 1, 1));
|
||||
VotingPluginWire.Vote vote = VotingPluginWire.readVote((JsonEnvelope) ReflectiveAccess.invoke(envelopeMethod, votingPluginProxy, new Object[]{cached, true, false, 1, 1}));
|
||||
|
||||
assertFalse(vote.queuedDeliveryKnown);
|
||||
assertFalse(vote.delayValidationKnown);
|
||||
@@ -1514,8 +1505,7 @@ public class VotingPluginProxyTest {
|
||||
String.class, boolean.class, boolean.class);
|
||||
receive.setAccessible(true);
|
||||
|
||||
receive.invoke(spyProxy, "Player", "Service", true, false, 100L, null,
|
||||
"00000000-0000-0000-0000-000000000001", java.util.UUID.randomUUID(), "Primary", true, true);
|
||||
ReflectiveAccess.invoke(receive, spyProxy, new Object[]{"Player", "Service", true, false, 100L, null, "00000000-0000-0000-0000-000000000001", java.util.UUID.randomUUID(), "Primary", true, true});
|
||||
|
||||
VotingPluginWire.Vote forwarded = VotingPluginWire.readVote(spyProxy.getLastVoteEnvelope());
|
||||
assertTrue(forwarded.delayValidationKnown);
|
||||
@@ -1546,8 +1536,7 @@ public class VotingPluginProxyTest {
|
||||
String.class, boolean.class, boolean.class);
|
||||
receive.setAccessible(true);
|
||||
|
||||
receive.invoke(spyProxy, "Player", "Service", true, false, 100L, null,
|
||||
"00000000-0000-0000-0000-000000000001", java.util.UUID.randomUUID(), "Legacy", false, false);
|
||||
ReflectiveAccess.invoke(receive, spyProxy, new Object[]{"Player", "Service", true, false, 100L, null, "00000000-0000-0000-0000-000000000001", java.util.UUID.randomUUID(), "Legacy", false, false});
|
||||
|
||||
VotingPluginWire.Vote forwarded = VotingPluginWire.readVote(spyProxy.getLastVoteEnvelope());
|
||||
assertFalse(forwarded.delayValidationKnown);
|
||||
@@ -1580,8 +1569,7 @@ public class VotingPluginProxyTest {
|
||||
String.class, boolean.class, boolean.class);
|
||||
receive.setAccessible(true);
|
||||
|
||||
receive.invoke(spyProxy, "Player", "Service", true, false, 100L, null,
|
||||
"00000000-0000-0000-0000-000000000001", java.util.UUID.randomUUID(), "Legacy", false, false);
|
||||
ReflectiveAccess.invoke(receive, spyProxy, new Object[]{"Player", "Service", true, false, 100L, null, "00000000-0000-0000-0000-000000000001", java.util.UUID.randomUUID(), "Legacy", false, false});
|
||||
|
||||
org.mockito.ArgumentCaptor<OfflineBungeeVote> cached =
|
||||
org.mockito.ArgumentCaptor.forClass(OfflineBungeeVote.class);
|
||||
@@ -1612,8 +1600,7 @@ public class VotingPluginProxyTest {
|
||||
com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class, String.class, java.util.UUID.class);
|
||||
receive.setAccessible(true);
|
||||
|
||||
receive.invoke(spyProxy, "Player", "Service", true, false, 100L, null,
|
||||
"00000000-0000-0000-0000-000000000001", voteId);
|
||||
ReflectiveAccess.invoke(receive, spyProxy, new Object[]{"Player", "Service", true, false, 100L, null, "00000000-0000-0000-0000-000000000001", voteId});
|
||||
org.mockito.ArgumentCaptor<Runnable> scheduled = org.mockito.ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(scheduler, Mockito.atLeastOnce()).schedule(scheduled.capture(), Mockito.anyLong(),
|
||||
Mockito.any(java.util.concurrent.TimeUnit.class));
|
||||
@@ -1623,8 +1610,7 @@ public class VotingPluginProxyTest {
|
||||
+ scheduled.getAllValues().stream().map(task -> task.getClass().getName()).toList()));
|
||||
voteRetry.run();
|
||||
// A duplicate socket/Redis delivery of the completed envelope is ignored.
|
||||
receive.invoke(spyProxy, "Player", "Service", true, false, 100L, null,
|
||||
"00000000-0000-0000-0000-000000000001", voteId);
|
||||
ReflectiveAccess.invoke(receive, spyProxy, new Object[]{"Player", "Service", true, false, 100L, null, "00000000-0000-0000-0000-000000000001", voteId});
|
||||
|
||||
verify(spyProxy).addVoteParty();
|
||||
java.lang.reflect.Field retries = VotingPluginProxy.class.getDeclaredField("liveVoteRetries");
|
||||
@@ -1654,8 +1640,7 @@ public class VotingPluginProxyTest {
|
||||
com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class, String.class, java.util.UUID.class);
|
||||
receive.setAccessible(true);
|
||||
|
||||
receive.invoke(spyProxy, "Player", "Service", true, false, 100L, null,
|
||||
"00000000-0000-0000-0000-000000000001", voteId);
|
||||
ReflectiveAccess.invoke(receive, spyProxy, new Object[]{"Player", "Service", true, false, 100L, null, "00000000-0000-0000-0000-000000000001", voteId});
|
||||
org.mockito.ArgumentCaptor<Runnable> scheduled = org.mockito.ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(scheduler).schedule(scheduled.capture(), Mockito.anyLong(),
|
||||
Mockito.any(java.util.concurrent.TimeUnit.class));
|
||||
@@ -1681,8 +1666,7 @@ public class VotingPluginProxyTest {
|
||||
com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class, String.class, java.util.UUID.class);
|
||||
receive.setAccessible(true);
|
||||
|
||||
receive.invoke(spyProxy, "Player", "Service", true, false, 100L, null,
|
||||
"00000000-0000-0000-0000-000000000001", voteId);
|
||||
ReflectiveAccess.invoke(receive, spyProxy, new Object[]{"Player", "Service", true, false, 100L, null, "00000000-0000-0000-0000-000000000001", voteId});
|
||||
|
||||
verify(spyProxy, never()).addVoteParty();
|
||||
}
|
||||
@@ -1709,8 +1693,7 @@ public class VotingPluginProxyTest {
|
||||
com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class, String.class, java.util.UUID.class);
|
||||
receive.setAccessible(true);
|
||||
|
||||
receive.invoke(spyProxy, "Player", "Service", false, true, 100L, null,
|
||||
"00000000-0000-0000-0000-000000000001", voteId);
|
||||
ReflectiveAccess.invoke(receive, spyProxy, new Object[]{"Player", "Service", false, true, 100L, null, "00000000-0000-0000-0000-000000000001", voteId});
|
||||
|
||||
assertEquals(1, queue.size());
|
||||
assertEquals(voteId, queue.element().getVoteId());
|
||||
@@ -1790,8 +1773,7 @@ public class VotingPluginProxyTest {
|
||||
com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class, String.class, java.util.UUID.class);
|
||||
receive.setAccessible(true);
|
||||
|
||||
receive.invoke(spyProxy, "Player", "Service", true, false, 100L, null,
|
||||
"00000000-0000-0000-0000-000000000001", voteId);
|
||||
ReflectiveAccess.invoke(receive, spyProxy, new Object[]{"Player", "Service", true, false, 100L, null, "00000000-0000-0000-0000-000000000001", voteId});
|
||||
spyProxy.onDisable(false);
|
||||
|
||||
verify(spyProxy).addVoteParty();
|
||||
@@ -1825,8 +1807,7 @@ public class VotingPluginProxyTest {
|
||||
com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class, String.class, java.util.UUID.class);
|
||||
receive.setAccessible(true);
|
||||
|
||||
receive.invoke(spyProxy, "Player", "Service", true, false, 100L, null,
|
||||
"00000000-0000-0000-0000-000000000001", voteId);
|
||||
ReflectiveAccess.invoke(receive, spyProxy, new Object[]{"Player", "Service", true, false, 100L, null, "00000000-0000-0000-0000-000000000001", voteId});
|
||||
spyProxy.onDisable(false);
|
||||
|
||||
java.lang.reflect.Field retries = VotingPluginProxy.class.getDeclaredField("multiProxyVoteRetries");
|
||||
@@ -1895,7 +1876,7 @@ public class VotingPluginProxyTest {
|
||||
Mockito.doReturn(voteCache).when(spyProxy).getVoteCacheHandler();
|
||||
java.lang.reflect.Field decider = VotingPluginProxy.class.getDeclaredField("proxyBroadcastDecider");
|
||||
decider.setAccessible(true);
|
||||
decider.set(spyProxy, new ProxyBroadcastDecider(spyProxy::getConfig, spyProxy::getAllAvailableServers,
|
||||
ReflectiveAccess.set(decider, spyProxy, new ProxyBroadcastDecider(spyProxy::getConfig, spyProxy::getAllAvailableServers,
|
||||
server -> true, server -> false));
|
||||
Mockito.doNothing().when(spyProxy).addVoteParty();
|
||||
Mockito.doReturn(true).when(spyProxy).checkVoteDelay(Mockito.anyString(), Mockito.anyString(),
|
||||
@@ -1931,8 +1912,7 @@ public class VotingPluginProxyTest {
|
||||
com.bencodez.votingplugin.proxy.VoteTotalsSnapshot.class, String.class, java.util.UUID.class);
|
||||
receive.setAccessible(true);
|
||||
|
||||
receive.invoke(spyProxy, "Player", "Service", true, false, 100L, null,
|
||||
"00000000-0000-0000-0000-000000000001", java.util.UUID.randomUUID());
|
||||
ReflectiveAccess.invoke(receive, spyProxy, new Object[]{"Player", "Service", true, false, 100L, null, "00000000-0000-0000-0000-000000000001", java.util.UUID.randomUUID()});
|
||||
|
||||
java.lang.reflect.Field liveRetries = VotingPluginProxy.class.getDeclaredField("liveVoteRetries");
|
||||
liveRetries.setAccessible(true);
|
||||
@@ -2830,7 +2810,7 @@ public class VotingPluginProxyTest {
|
||||
|
||||
java.lang.reflect.Method start = VotingPluginProxy.class.getDeclaredMethod("startHttpTransportOrFallBack");
|
||||
start.setAccessible(true);
|
||||
start.invoke(spyProxy);
|
||||
ReflectiveAccess.invoke(start, spyProxy, new Object[]{});
|
||||
|
||||
assertEquals(BungeeMethod.PLUGINMESSAGING, spyProxy.getMethod());
|
||||
assertFalse(java.nio.file.Files.exists(retained));
|
||||
@@ -2849,7 +2829,7 @@ public class VotingPluginProxyTest {
|
||||
.deleteRetainedHttpListenerSettings(retained);
|
||||
java.lang.reflect.Method start = VotingPluginProxy.class.getDeclaredMethod("startHttpTransportOrFallBack");
|
||||
start.setAccessible(true);
|
||||
start.invoke(spyProxy);
|
||||
ReflectiveAccess.invoke(start, spyProxy, new Object[]{});
|
||||
assertEquals(BungeeMethod.PLUGINMESSAGING, spyProxy.getMethod());
|
||||
assertTrue(java.nio.file.Files.isRegularFile(retained));
|
||||
spyProxy.reloadFromControl();
|
||||
@@ -2867,12 +2847,12 @@ public class VotingPluginProxyTest {
|
||||
java.lang.reflect.Method start = VotingPluginProxy.class.getDeclaredMethod("startHttpTransportOrFallBack");
|
||||
start.setAccessible(true);
|
||||
|
||||
start.invoke(votingPluginProxy);
|
||||
ReflectiveAccess.invoke(start, votingPluginProxy, new Object[]{});
|
||||
|
||||
Object handler = getProxyField(votingPluginProxy, "encryptionHandler");
|
||||
assertTrue(handler != null);
|
||||
votingPluginProxy.setMethod(BungeeMethod.HTTP);
|
||||
start.invoke(votingPluginProxy);
|
||||
ReflectiveAccess.invoke(start, votingPluginProxy, new Object[]{});
|
||||
assertSame(handler, getProxyField(votingPluginProxy, "encryptionHandler"));
|
||||
}
|
||||
|
||||
@@ -2887,7 +2867,7 @@ public class VotingPluginProxyTest {
|
||||
Mockito.when(votingPluginProxy.getConfig().getHttpPublicEndpoint()).thenReturn("");
|
||||
|
||||
java.lang.reflect.InvocationTargetException failure = assertThrows(
|
||||
java.lang.reflect.InvocationTargetException.class, () -> start.invoke(votingPluginProxy), configured.name());
|
||||
java.lang.reflect.InvocationTargetException.class, () -> ReflectiveAccess.invoke(start, votingPluginProxy, new Object[]{}), configured.name());
|
||||
|
||||
assertTrue(failure.getCause() instanceof IllegalStateException, configured.name());
|
||||
assertEquals(BungeeMethod.HTTP, votingPluginProxy.getMethod(), configured.name());
|
||||
@@ -3457,7 +3437,7 @@ public class VotingPluginProxyTest {
|
||||
java.lang.reflect.Method probe = VotingPluginProxy.class
|
||||
.getDeclaredMethod("probeReliableVoteDeliveryCapabilities");
|
||||
probe.setAccessible(true);
|
||||
probe.invoke(spyProxy);
|
||||
ReflectiveAccess.invoke(probe, spyProxy, new Object[]{});
|
||||
|
||||
org.mockito.ArgumentCaptor<String> target = org.mockito.ArgumentCaptor.forClass(String.class);
|
||||
org.mockito.ArgumentCaptor<JsonEnvelope> envelope = org.mockito.ArgumentCaptor.forClass(JsonEnvelope.class);
|
||||
@@ -4107,7 +4087,7 @@ public class VotingPluginProxyTest {
|
||||
private static void setProxyField(VotingPluginProxy target, String name, Object value) throws Exception {
|
||||
java.lang.reflect.Field field = VotingPluginProxy.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
ReflectiveAccess.set(field, target, value);
|
||||
}
|
||||
|
||||
private static Object getProxyField(VotingPluginProxy target, String name) throws Exception {
|
||||
|
||||
+5
-3
@@ -1,5 +1,7 @@
|
||||
package com.bencodez.votingplugin.tests;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -496,7 +498,7 @@ public class VotingPluginProxyTestImpl extends VotingPluginProxy {
|
||||
HttpProxyTransportServer.DeliveryRetryException.class
|
||||
.getDeclaredConstructor(String.class, Throwable.class);
|
||||
constructor.setAccessible(true);
|
||||
throw constructor.newInstance(generatedHttpRetryId, new IllegalStateException("publication ambiguous"));
|
||||
throw ReflectiveAccess.newInstance(constructor, new Object[]{generatedHttpRetryId, new IllegalStateException("publication ambiguous")});
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new AssertionError(failure);
|
||||
}
|
||||
@@ -557,7 +559,7 @@ public class VotingPluginProxyTestImpl extends VotingPluginProxy {
|
||||
HttpProxyTransportServer.DeliveryRetryException.class
|
||||
.getDeclaredConstructor(String.class, Throwable.class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(deliveryId, new IllegalStateException("publication ambiguous"));
|
||||
return ReflectiveAccess.newInstance(constructor, new Object[]{deliveryId, new IllegalStateException("publication ambiguous")});
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new AssertionError(failure);
|
||||
}
|
||||
@@ -628,7 +630,7 @@ public class VotingPluginProxyTestImpl extends VotingPluginProxy {
|
||||
try {
|
||||
java.lang.reflect.Field field = VotingPluginProxy.class.getDeclaredField("globalMessageProxyHandler");
|
||||
field.setAccessible(true);
|
||||
field.set(this, handler);
|
||||
ReflectiveAccess.set(field, this, handler);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.bencodez.votingplugin.tests.backendproxy;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.backendproxy.*;
|
||||
import com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue.*;
|
||||
import java.util.function.Consumer;
|
||||
import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope;
|
||||
|
||||
/** Typed test-only access to package-private BackendOrderedVoteOverflowQueue seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class BackendOrderedVoteOverflowQueueAccess {
|
||||
public static final int MAX_NORMAL_ENTRIES = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue", "MAX_NORMAL_ENTRIES");
|
||||
public static final int MAX_FAILED_ENTRIES = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue", "MAX_FAILED_ENTRIES");
|
||||
private BackendOrderedVoteOverflowQueueAccess() { }
|
||||
public static boolean enqueue(BackendOrderedVoteOverflowQueue target, JsonEnvelope envelope) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue", target, "enqueue", new String[]{"JsonEnvelope"}, new Object[]{envelope}); }
|
||||
public static boolean enqueue(BackendOrderedVoteOverflowQueue target, JsonEnvelope envelope, int reservedPrefixEntries) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue", target, "enqueue", new String[]{"JsonEnvelope", "int"}, new Object[]{envelope, reservedPrefixEntries}); }
|
||||
public static int failedSize(BackendOrderedVoteOverflowQueue target) { return (Integer) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue", target, "failedSize", new String[]{}, new Object[]{}); }
|
||||
public static boolean isQuarantineCapacityExhausted(BackendOrderedVoteOverflowQueue target) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue", target, "isQuarantineCapacityExhausted", new String[]{}, new Object[]{}); }
|
||||
public static boolean quarantineForShutdown(BackendOrderedVoteOverflowQueue target, BackendOrderedVoteOverflowQueueAccess.PendingEnvelope expected, JsonEnvelope envelope) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue", target, "quarantineForShutdown", new String[]{"PendingEnvelope", "JsonEnvelope"}, new Object[]{expected, envelope}); }
|
||||
public static void quarantineAsync(BackendOrderedVoteOverflowQueue target, BackendOrderedVoteOverflowQueueAccess.PendingEnvelope expected, JsonEnvelope envelope, Consumer<Boolean> completion) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue", target, "quarantineAsync", new String[]{"PendingEnvelope", "JsonEnvelope", "Consumer"}, new Object[]{expected, envelope, completion}); }
|
||||
public static BackendOrderedVoteOverflowQueueAccess.PendingEnvelope peekDurable(BackendOrderedVoteOverflowQueue target) { return BackendOrderedVoteOverflowQueueAccess.PendingEnvelope.view(ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue", target, "peekDurable", new String[]{}, new Object[]{})); }
|
||||
public static boolean awaitClose(BackendOrderedVoteOverflowQueue target, long timeoutMillis) throws InterruptedException { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue", target, "awaitClose", new String[]{"long"}, new Object[]{timeoutMillis}); }
|
||||
public static BackendOrderedVoteOverflowQueueAccess.CloseState closeState(BackendOrderedVoteOverflowQueue target) { return ReflectiveAccess.enumView(BackendOrderedVoteOverflowQueueAccess.CloseState.class, ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue", target, "closeState", new String[]{}, new Object[]{})); }
|
||||
public enum CloseState { OPEN, CLOSING, WAITING_TO_PERSIST, SNAPSHOT_REPLACED, COMPLETE, FAILED }
|
||||
|
||||
public static final class PendingEnvelope implements ReflectiveAccess.View {
|
||||
private final Object delegate;
|
||||
private PendingEnvelope(Object delegate) { this.delegate = delegate; }
|
||||
public static PendingEnvelope view(Object delegate) { return delegate == null ? null : new PendingEnvelope(delegate); }
|
||||
@Override public Object delegate() { return delegate; }
|
||||
@Override public boolean equals(Object other) { return other instanceof PendingEnvelope value && delegate.equals(value.delegate); }
|
||||
@Override public int hashCode() { return delegate.hashCode(); }
|
||||
public JsonEnvelope envelope() { return (JsonEnvelope) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue$PendingEnvelope", delegate, "envelope", new String[]{}, new Object[]{}); }
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.bencodez.votingplugin.tests.backendproxy;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.backendproxy.*;
|
||||
import com.bencodez.votingplugin.backendproxy.BackendProxyHandler.*;
|
||||
import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope;
|
||||
|
||||
/** Typed test-only access to package-private BackendProxyHandler seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class BackendProxyHandlerAccess {
|
||||
public static void dispatchIncomingAfterPublication(BackendProxyHandler target, JsonEnvelope envelope, Runnable localDispatch) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.BackendProxyHandler", target, "dispatchIncomingAfterPublication", new String[]{"JsonEnvelope", "Runnable"}, new Object[]{envelope, localDispatch}); }
|
||||
public static final int MAX_ORDERED_VOTE_QUEUE = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.BackendProxyHandler", "MAX_ORDERED_VOTE_QUEUE");
|
||||
private BackendProxyHandlerAccess() { }
|
||||
|
||||
}
|
||||
+125
-119
@@ -1,4 +1,10 @@
|
||||
package com.bencodez.votingplugin.backendproxy;
|
||||
package com.bencodez.votingplugin.tests.backendproxy;
|
||||
|
||||
import com.bencodez.votingplugin.tests.backendproxy.cache.ProcessedVoteCacheAccess;
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import com.bencodez.votingplugin.backendproxy.BackendOrderedVoteOverflowQueue;
|
||||
import com.bencodez.votingplugin.backendproxy.BackendProxyHandler;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -197,7 +203,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
TransportEnvelopeEncryption.Domain.PROXY_BACKEND, false));
|
||||
BackendProxyTransportManager manager = (BackendProxyTransportManager) getField(handler, "transportManager");
|
||||
BackendProxyTransport transport = method == BungeeMethod.REDIS
|
||||
? new RedisBackendProxyTransport(plugin, new ProcessedVoteCache())
|
||||
? new RedisBackendProxyTransport(plugin, ProcessedVoteCacheAccess.create())
|
||||
: new MqttBackendProxyTransport(plugin);
|
||||
setField(transport, "authenticator", SharedTransportEnvelopeAuthenticator.load(keyFile,
|
||||
Mode.COMPATIBILITY));
|
||||
@@ -233,7 +239,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
setField(handler, "method", BungeeMethod.REDIS);
|
||||
setField(handler, "sharedTransportMode", Mode.COMPATIBILITY);
|
||||
setField(handler, "communicationEncryptionEnabled", false);
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(plugin, new ProcessedVoteCache());
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(plugin, ProcessedVoteCacheAccess.create());
|
||||
SharedTransportEnvelopeAuthenticator original = SharedTransportEnvelopeAuthenticator.load(
|
||||
dataDirectory.resolve("secretkey.key"), Mode.COMPATIBILITY);
|
||||
setField(transport, "authenticator", original);
|
||||
@@ -267,7 +273,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
setField(handler, "communicationEncryption", originalEncryption);
|
||||
BackendProxyTransportManager manager = (BackendProxyTransportManager) getField(handler, "transportManager");
|
||||
BackendProxyTransport transport = method == BungeeMethod.REDIS
|
||||
? new RedisBackendProxyTransport(plugin, new ProcessedVoteCache())
|
||||
? new RedisBackendProxyTransport(plugin, ProcessedVoteCacheAccess.create())
|
||||
: new MqttBackendProxyTransport(plugin);
|
||||
SharedTransportEnvelopeAuthenticator originalAuthenticator =
|
||||
SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED);
|
||||
@@ -298,7 +304,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
handler.activateInboundMessages();
|
||||
Runnable localDispatch = mock(Runnable.class);
|
||||
|
||||
handler.dispatchIncomingAfterPublication(
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler,
|
||||
JsonEnvelope.builder(VotingPluginWire.SUB_BUNGEE_TIME_CHANGE).build(), localDispatch);
|
||||
|
||||
verify(scheduler).runTaskAsynchronously(plugin, localDispatch);
|
||||
@@ -338,9 +344,9 @@ class BackendProxyHandlerLifecycleTest {
|
||||
return null;
|
||||
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
|
||||
|
||||
handler.dispatchIncomingAfterPublication(voteEnvelope, mock(Runnable.class));
|
||||
handler.dispatchIncomingAfterPublication(updateEnvelope, mock(Runnable.class));
|
||||
handler.dispatchIncomingAfterPublication(onlineEnvelope, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, voteEnvelope, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, updateEnvelope, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, onlineEnvelope, mock(Runnable.class));
|
||||
|
||||
assertEquals(1, asyncTasks.size(), "only the head of the ordered vote lane should be scheduled");
|
||||
|
||||
@@ -391,9 +397,9 @@ class BackendProxyHandlerLifecycleTest {
|
||||
return null;
|
||||
}).when(router).handleOrderedVote(eq(release), any());
|
||||
|
||||
handler.dispatchIncomingAfterPublication(blockedVote, mock(Runnable.class));
|
||||
handler.dispatchIncomingAfterPublication(release, mock(Runnable.class));
|
||||
handler.dispatchIncomingAfterPublication(secondRelease, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, blockedVote, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, release, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, secondRelease, mock(Runnable.class));
|
||||
|
||||
assertEquals(2, asyncTasks.size(), "only one durable release should use the bounded release worker");
|
||||
asyncTasks.removeLast().run();
|
||||
@@ -419,7 +425,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
doThrow(new java.util.concurrent.RejectedExecutionException("stopping"))
|
||||
.when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
|
||||
|
||||
handler.dispatchIncomingAfterPublication(release, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, release, mock(Runnable.class));
|
||||
|
||||
verify(router, never()).handleOrderedVote(eq(release), any());
|
||||
AtomicBoolean active = (AtomicBoolean) getField(handler, "durableReceiptReleaseActive");
|
||||
@@ -482,8 +488,8 @@ class BackendProxyHandlerLifecycleTest {
|
||||
"survival", UUID.randomUUID(), VotingPluginWire.SUB_VOTE);
|
||||
when(router.isValidReceiptRelease(release)).thenReturn(true);
|
||||
|
||||
handler.dispatchIncomingAfterPublication(olderVote, mock(Runnable.class));
|
||||
handler.dispatchIncomingAfterPublication(release, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, olderVote, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, release, mock(Runnable.class));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArrayDeque<JsonEnvelope> queued = (ArrayDeque<JsonEnvelope>) getField(handler, "orderedVoteDispatchQueue");
|
||||
@@ -521,7 +527,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
VotingPluginWire.vote("Player", UUID.randomUUID().toString(), "site", 10L,
|
||||
true, true, "", voteId, false, false, 1, 1));
|
||||
|
||||
handler.dispatchIncomingAfterPublication(envelope, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, envelope, mock(Runnable.class));
|
||||
dispatch.get().run();
|
||||
verifyNoInteractions(messages);
|
||||
completion.get().accept(OrderedVoteOutcome.COMPLETE);
|
||||
@@ -560,7 +566,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
VotingPluginWire.voteDelayRejected("Player", UUID.randomUUID().toString(),
|
||||
"site", true, voteId));
|
||||
|
||||
handler.dispatchIncomingAfterPublication(envelope, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, envelope, mock(Runnable.class));
|
||||
dispatch.get().run();
|
||||
verifyNoInteractions(messages);
|
||||
completion.get().accept(OrderedVoteOutcome.COMPLETE);
|
||||
@@ -580,19 +586,19 @@ class BackendProxyHandlerLifecycleTest {
|
||||
when(plugin.getLogger()).thenReturn(Logger.getLogger("ordered-overflow-test"));
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
setField(handler, "messageRouter", mock(BackendProxyMessageRouter.class));
|
||||
handler.activateInboundMessages();
|
||||
|
||||
for (int i = 0; i < BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE + 1; i++) {
|
||||
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
for (int i = 0; i < BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE + 1; i++) {
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put(VotingPluginWire.K_VOTE_ID, new java.util.UUID(0L, i + 1L).toString()).build(),
|
||||
mock(Runnable.class));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArrayDeque<JsonEnvelope> queued = (ArrayDeque<JsonEnvelope>) getField(handler, "orderedVoteDispatchQueue");
|
||||
assertEquals(BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE, queued.size());
|
||||
assertEquals(BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE, queued.size());
|
||||
assertEquals(1, overflow.size());
|
||||
} finally {
|
||||
overflow.close();
|
||||
@@ -650,14 +656,14 @@ class BackendProxyHandlerLifecycleTest {
|
||||
blocker.start();
|
||||
assertTrue(locked.await(3, TimeUnit.SECONDS));
|
||||
try {
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
setField(handler, "messageRouter", mock(BackendProxyMessageRouter.class));
|
||||
handler.activateInboundMessages();
|
||||
for (int index = 0; index < BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE; index++) {
|
||||
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
for (int index = 0; index < BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE; index++) {
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", Integer.toString(index)).build(), mock(Runnable.class));
|
||||
}
|
||||
CompletableFuture<Void> admission = CompletableFuture.runAsync(() -> handler.dispatchIncomingAfterPublication(
|
||||
CompletableFuture<Void> admission = CompletableFuture.runAsync(() -> BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler,
|
||||
JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).put("sequence", "overflow").build(),
|
||||
mock(Runnable.class)));
|
||||
admission.get(500, TimeUnit.MILLISECONDS);
|
||||
@@ -684,7 +690,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
when(plugin.getLogger()).thenReturn(Logger.getLogger("ordered-unreadable-test"));
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
assertFalse(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
|
||||
assertFalse(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
|
||||
} finally {
|
||||
overflow.close();
|
||||
}
|
||||
@@ -705,7 +711,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
Files.writeString(queueFile, malformed);
|
||||
BackendOrderedVoteOverflowQueue malformedQueue = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
assertFalse(malformedQueue.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
|
||||
assertFalse(BackendOrderedVoteOverflowQueueAccess.enqueue(malformedQueue, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
|
||||
} finally {
|
||||
malformedQueue.close();
|
||||
}
|
||||
@@ -719,7 +725,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
Files.writeString(queueFile, unsupported);
|
||||
BackendOrderedVoteOverflowQueue unsupportedQueue = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
assertFalse(unsupportedQueue.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
|
||||
assertFalse(BackendOrderedVoteOverflowQueueAccess.enqueue(unsupportedQueue, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
|
||||
} finally {
|
||||
unsupportedQueue.close();
|
||||
}
|
||||
@@ -734,7 +740,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
Files.createDirectory(tempDir.resolve("BackendProxyVoteQueue.yml"));
|
||||
assertFalse(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
|
||||
assertFalse(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
|
||||
assertEquals(0, overflow.size());
|
||||
} finally {
|
||||
overflow.close();
|
||||
@@ -744,7 +750,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
@Test
|
||||
void oversizedFailureHistoryStopsRecoveryWithoutDiscardingEvidence(@TempDir Path tempDir) throws Exception {
|
||||
StringBuilder yaml = new StringBuilder("Envelopes: []\nFailedEnvelopes:\n");
|
||||
for (int index = 0; index < BackendOrderedVoteOverflowQueue.MAX_FAILED_ENTRIES + 20; index++) {
|
||||
for (int index = 0; index < BackendOrderedVoteOverflowQueueAccess.MAX_FAILED_ENTRIES + 20; index++) {
|
||||
yaml.append("- failed-").append(index).append('\n');
|
||||
}
|
||||
Files.writeString(tempDir.resolve("BackendProxyVoteQueue.yml"), yaml);
|
||||
@@ -753,8 +759,8 @@ class BackendProxyHandlerLifecycleTest {
|
||||
when(plugin.getLogger()).thenReturn(Logger.getLogger("ordered-failure-bound-test"));
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
assertEquals(0, overflow.failedSize());
|
||||
assertFalse(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
|
||||
assertEquals(0, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
|
||||
assertFalse(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
|
||||
assertTrue(Files.readString(tempDir.resolve("BackendProxyVoteQueue.yml")).contains("failed-0"));
|
||||
} finally {
|
||||
overflow.close();
|
||||
@@ -764,7 +770,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
@Test
|
||||
void fullFailureHistoryRejectsNextQuarantineWithoutEviction(@TempDir Path tempDir) throws Exception {
|
||||
StringBuilder yaml = new StringBuilder("Envelopes: []\nFailedEnvelopes:\n");
|
||||
for (int index = 0; index < BackendOrderedVoteOverflowQueue.MAX_FAILED_ENTRIES; index++) {
|
||||
for (int index = 0; index < BackendOrderedVoteOverflowQueueAccess.MAX_FAILED_ENTRIES; index++) {
|
||||
yaml.append("- failed-").append(index).append('\n');
|
||||
}
|
||||
Files.writeString(tempDir.resolve("BackendProxyVoteQueue.yml"), yaml);
|
||||
@@ -773,10 +779,10 @@ class BackendProxyHandlerLifecycleTest {
|
||||
when(plugin.getLogger()).thenReturn(Logger.getLogger("ordered-failure-capacity-test"));
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
assertFalse(overflow.quarantineForShutdown(null, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
assertFalse(BackendOrderedVoteOverflowQueueAccess.quarantineForShutdown(overflow, null, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", "rejected").build()));
|
||||
assertEquals(BackendOrderedVoteOverflowQueue.MAX_FAILED_ENTRIES, overflow.failedSize());
|
||||
assertTrue(overflow.isQuarantineCapacityExhausted());
|
||||
assertEquals(BackendOrderedVoteOverflowQueueAccess.MAX_FAILED_ENTRIES, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
|
||||
assertTrue(BackendOrderedVoteOverflowQueueAccess.isQuarantineCapacityExhausted(overflow));
|
||||
@SuppressWarnings("unchecked")
|
||||
ArrayDeque<String> failures = (ArrayDeque<String>) getField(overflow, "failedEntries");
|
||||
assertEquals("failed-0", failures.peekFirst());
|
||||
@@ -784,9 +790,9 @@ class BackendProxyHandlerLifecycleTest {
|
||||
|
||||
BukkitScheduler scheduler = mock(BukkitScheduler.class);
|
||||
when(plugin.getBukkitScheduler()).thenReturn(scheduler);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
handler.activateInboundMessages();
|
||||
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build(),
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build(),
|
||||
mock(Runnable.class));
|
||||
verify(scheduler, never()).runTaskAsynchronously(eq(plugin), any(Runnable.class));
|
||||
} finally {
|
||||
@@ -804,19 +810,19 @@ class BackendProxyHandlerLifecycleTest {
|
||||
UUID.randomUUID().toString(), "site", 10L, true, true, "", voteId, false, false, 1, 1));
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
assertTrue(overflow.quarantineForShutdown(null, vote));
|
||||
assertTrue(BackendOrderedVoteOverflowQueueAccess.quarantineForShutdown(overflow, null, vote));
|
||||
long firstDeadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
|
||||
while (overflow.failedSize() != 1 && System.nanoTime() < firstDeadline) Thread.sleep(10L);
|
||||
assertEquals(1, overflow.failedSize());
|
||||
while (BackendOrderedVoteOverflowQueueAccess.failedSize(overflow) != 1 && System.nanoTime() < firstDeadline) Thread.sleep(10L);
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
|
||||
|
||||
assertTrue(overflow.enqueue(vote));
|
||||
BackendOrderedVoteOverflowQueue.PendingEnvelope retry = overflow.peekDurable();
|
||||
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, vote));
|
||||
BackendOrderedVoteOverflowQueueAccess.PendingEnvelope retry = BackendOrderedVoteOverflowQueueAccess.peekDurable(overflow);
|
||||
assertNotNull(retry);
|
||||
CompletableFuture<Boolean> quarantined = new CompletableFuture<>();
|
||||
overflow.quarantineAsync(retry, vote, quarantined::complete);
|
||||
BackendOrderedVoteOverflowQueueAccess.quarantineAsync(overflow, retry, vote, quarantined::complete);
|
||||
|
||||
assertTrue(quarantined.get(3, TimeUnit.SECONDS));
|
||||
assertEquals(1, overflow.failedSize());
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
|
||||
assertEquals(0, overflow.size());
|
||||
} finally {
|
||||
overflow.close();
|
||||
@@ -824,7 +830,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
|
||||
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
assertEquals(1, recovered.failedSize());
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(recovered));
|
||||
assertEquals(0, recovered.size());
|
||||
} finally {
|
||||
recovered.close();
|
||||
@@ -845,7 +851,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
}).when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
|
||||
setField(handler, "messageRouter", router);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
@@ -856,9 +862,9 @@ class BackendProxyHandlerLifecycleTest {
|
||||
return null;
|
||||
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
|
||||
handler.activateInboundMessages();
|
||||
assertTrue(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", "failed").build()));
|
||||
assertTrue(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", "next").build()));
|
||||
Runnable first = asyncTasks.poll(3, TimeUnit.SECONDS);
|
||||
assertNotNull(first);
|
||||
@@ -870,7 +876,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
|
||||
while (overflow.size() != 0 && System.nanoTime() < deadline) Thread.sleep(10);
|
||||
assertEquals(0, overflow.size());
|
||||
assertEquals(1, overflow.failedSize());
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
|
||||
assertTrue(Files.exists(tempDir.resolve("BackendProxyVoteQueue.yml")));
|
||||
} finally {
|
||||
overflow.close();
|
||||
@@ -878,7 +884,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
assertEquals(0, recovered.size());
|
||||
assertEquals(1, recovered.failedSize());
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(recovered));
|
||||
} finally {
|
||||
recovered.close();
|
||||
}
|
||||
@@ -898,22 +904,22 @@ class BackendProxyHandlerLifecycleTest {
|
||||
}).when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
|
||||
setField(handler, "messageRouter", router);
|
||||
doThrow(new IllegalStateException("partial reward")).when(router)
|
||||
.handleOrderedVote(any(JsonEnvelope.class), any());
|
||||
handler.activateInboundMessages();
|
||||
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", "failed").build(), mock(Runnable.class));
|
||||
Runnable first = asyncTasks.poll(3, TimeUnit.SECONDS);
|
||||
assertNotNull(first);
|
||||
assertThrows(IllegalStateException.class, first::run);
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
|
||||
while (overflow.failedSize() == 0 && System.nanoTime() < deadline) {
|
||||
while (BackendOrderedVoteOverflowQueueAccess.failedSize(overflow) == 0 && System.nanoTime() < deadline) {
|
||||
Thread.sleep(10);
|
||||
}
|
||||
assertEquals(1, overflow.failedSize());
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
|
||||
assertEquals(0, overflow.size());
|
||||
assertTrue(asyncTasks.isEmpty(), "failed reward must not be retried");
|
||||
} finally {
|
||||
@@ -947,13 +953,13 @@ class BackendProxyHandlerLifecycleTest {
|
||||
});
|
||||
assertTrue(occupied.await(3, TimeUnit.SECONDS));
|
||||
try {
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
|
||||
setField(handler, "messageRouter", router);
|
||||
doThrow(new IllegalStateException("partial reward")).when(router)
|
||||
.handleOrderedVote(any(JsonEnvelope.class), any());
|
||||
handler.activateInboundMessages();
|
||||
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", "failed").build(), mock(Runnable.class));
|
||||
Runnable first = asyncTasks.poll(3, TimeUnit.SECONDS);
|
||||
assertNotNull(first);
|
||||
@@ -966,7 +972,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
assertEquals(0, recovered.size());
|
||||
assertEquals(1, recovered.failedSize());
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(recovered));
|
||||
} finally {
|
||||
recovered.close();
|
||||
}
|
||||
@@ -980,29 +986,29 @@ class BackendProxyHandlerLifecycleTest {
|
||||
when(plugin.getLogger()).thenReturn(Logger.getLogger("ordered-capacity-test"));
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
handler.activateInboundMessages();
|
||||
for (int index = 0; index < BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE; index++) {
|
||||
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
for (int index = 0; index < BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE; index++) {
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", Integer.toString(index)).build(), mock(Runnable.class));
|
||||
}
|
||||
for (int index = 0; index < BackendOrderedVoteOverflowQueue.MAX_NORMAL_ENTRIES; index++) {
|
||||
assertTrue(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", Integer.toString(index + BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE))
|
||||
.build(), BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE));
|
||||
for (int index = 0; index < BackendOrderedVoteOverflowQueueAccess.MAX_NORMAL_ENTRIES; index++) {
|
||||
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", Integer.toString(index + BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE))
|
||||
.build(), BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE));
|
||||
}
|
||||
assertFalse(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build(),
|
||||
BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE));
|
||||
assertFalse(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build(),
|
||||
BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE));
|
||||
handler.close();
|
||||
assertEquals(BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE
|
||||
+ BackendOrderedVoteOverflowQueue.MAX_NORMAL_ENTRIES, overflow.size());
|
||||
assertEquals(BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE
|
||||
+ BackendOrderedVoteOverflowQueueAccess.MAX_NORMAL_ENTRIES, overflow.size());
|
||||
} finally {
|
||||
overflow.close();
|
||||
}
|
||||
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
assertEquals(BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE
|
||||
+ BackendOrderedVoteOverflowQueue.MAX_NORMAL_ENTRIES, recovered.size());
|
||||
assertEquals(BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE
|
||||
+ BackendOrderedVoteOverflowQueueAccess.MAX_NORMAL_ENTRIES, recovered.size());
|
||||
} finally {
|
||||
recovered.close();
|
||||
}
|
||||
@@ -1022,7 +1028,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
}).when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
|
||||
setField(handler, "messageRouter", router);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
@@ -1033,7 +1039,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
return null;
|
||||
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
|
||||
handler.activateInboundMessages();
|
||||
assertTrue(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build()));
|
||||
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build()));
|
||||
|
||||
Runnable first = asyncTasks.poll(3, TimeUnit.SECONDS);
|
||||
assertNotNull(first, "durable head must wake the ordered lane");
|
||||
@@ -1067,7 +1073,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
}).when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
|
||||
setField(handler, "messageRouter", router);
|
||||
doAnswer(invocation -> {
|
||||
@@ -1076,7 +1082,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
return null;
|
||||
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
|
||||
handler.activateInboundMessages();
|
||||
assertTrue(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
|
||||
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
|
||||
asyncTasks.poll(3, TimeUnit.SECONDS).run();
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
|
||||
while (overflow.size() != 0 && System.nanoTime() < deadline) Thread.sleep(10);
|
||||
@@ -1118,11 +1124,11 @@ class BackendProxyHandlerLifecycleTest {
|
||||
try {
|
||||
overflow.close();
|
||||
assertTrue(System.nanoTime() - started < TimeUnit.SECONDS.toNanos(4));
|
||||
assertEquals(BackendOrderedVoteOverflowQueue.CloseState.WAITING_TO_PERSIST, overflow.closeState());
|
||||
assertEquals(BackendOrderedVoteOverflowQueueAccess.CloseState.WAITING_TO_PERSIST, BackendOrderedVoteOverflowQueueAccess.closeState(overflow));
|
||||
} finally {
|
||||
release.countDown();
|
||||
blocker.join(3_000L);
|
||||
assertTrue(overflow.awaitClose(3_000L));
|
||||
assertTrue(BackendOrderedVoteOverflowQueueAccess.awaitClose(overflow, 3_000L));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1137,7 +1143,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
when(plugin.getDataFolder()).thenReturn(tempDir.toFile());
|
||||
when(plugin.getLogger()).thenReturn(logger);
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
assertTrue(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).put("sequence", "slow").build()));
|
||||
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).put("sequence", "slow").build()));
|
||||
Object persistenceLock = getField(overflow, "persistenceWriteLock");
|
||||
CountDownLatch locked = new CountDownLatch(1);
|
||||
Thread blocker = new Thread(() -> {
|
||||
@@ -1156,7 +1162,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
overflow.close();
|
||||
blocker.join(3_000L);
|
||||
assertTrue(System.nanoTime() - started >= TimeUnit.SECONDS.toNanos(1));
|
||||
assertEquals(BackendOrderedVoteOverflowQueue.CloseState.COMPLETE, overflow.closeState());
|
||||
assertEquals(BackendOrderedVoteOverflowQueueAccess.CloseState.COMPLETE, BackendOrderedVoteOverflowQueueAccess.closeState(overflow));
|
||||
assertFalse(logs.hasLevel(Level.SEVERE));
|
||||
|
||||
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
@@ -1183,7 +1189,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
overflow.close();
|
||||
|
||||
assertEquals(BackendOrderedVoteOverflowQueue.CloseState.FAILED, overflow.closeState());
|
||||
assertEquals(BackendOrderedVoteOverflowQueueAccess.CloseState.FAILED, BackendOrderedVoteOverflowQueueAccess.closeState(overflow));
|
||||
assertTrue(logs.hasLevel(Level.SEVERE));
|
||||
LogRecord failure = logs.records.stream()
|
||||
.filter(record -> record.getLevel() == Level.SEVERE)
|
||||
@@ -1223,9 +1229,9 @@ class BackendProxyHandlerLifecycleTest {
|
||||
return null;
|
||||
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
|
||||
handler.activateInboundMessages();
|
||||
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE)
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE)
|
||||
.put("sequence", "first").build(), mock(Runnable.class));
|
||||
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", "second").build(), mock(Runnable.class));
|
||||
|
||||
asyncTasks.removeFirst().run();
|
||||
@@ -1246,12 +1252,12 @@ class BackendProxyHandlerLifecycleTest {
|
||||
when(plugin.getLogger()).thenReturn(Logger.getLogger("ordered-shutdown-test"));
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
BackendProxyTransportManager transport = mock(BackendProxyTransportManager.class);
|
||||
setField(handler, "transportManager", transport);
|
||||
handler.activateInboundMessages();
|
||||
doAnswer(invocation -> {
|
||||
handler.dispatchIncomingAfterPublication(
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler,
|
||||
JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build(), mock(Runnable.class));
|
||||
return null;
|
||||
}).when(transport).close();
|
||||
@@ -1283,7 +1289,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
}).when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
|
||||
setField(handler, "messageRouter", router);
|
||||
AtomicReference<java.util.function.Consumer<OrderedVoteOutcome>> completion = new AtomicReference<>();
|
||||
@@ -1292,9 +1298,9 @@ class BackendProxyHandlerLifecycleTest {
|
||||
return null;
|
||||
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
|
||||
handler.activateInboundMessages();
|
||||
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", "active").build(), mock(Runnable.class));
|
||||
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", "later").build(), mock(Runnable.class));
|
||||
asyncTasks.removeFirst().run();
|
||||
CompletableFuture<Void> closing = CompletableFuture.runAsync(handler::close);
|
||||
@@ -1323,7 +1329,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
AtomicReference<java.util.function.Consumer<OrderedVoteOutcome>> completion = new AtomicReference<>();
|
||||
try {
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
|
||||
setField(handler, "messageRouter", router);
|
||||
doAnswer(invocation -> {
|
||||
@@ -1331,26 +1337,26 @@ class BackendProxyHandlerLifecycleTest {
|
||||
return null;
|
||||
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
|
||||
handler.activateInboundMessages();
|
||||
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", "active").build(), mock(Runnable.class));
|
||||
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", "later").build(), mock(Runnable.class));
|
||||
asyncTasks.removeFirst().run();
|
||||
|
||||
CompletableFuture.runAsync(handler::close).get(3, TimeUnit.SECONDS);
|
||||
long quarantineDeadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
|
||||
while (overflow.failedSize() != 1 && System.nanoTime() < quarantineDeadline) Thread.sleep(10L);
|
||||
assertEquals(1, overflow.failedSize());
|
||||
while (BackendOrderedVoteOverflowQueueAccess.failedSize(overflow) != 1 && System.nanoTime() < quarantineDeadline) Thread.sleep(10L);
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
|
||||
assertEquals(1, overflow.size());
|
||||
completion.get().accept(OrderedVoteOutcome.COMPLETE);
|
||||
assertEquals(1, overflow.failedSize());
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
|
||||
assertEquals(1, overflow.size());
|
||||
} finally {
|
||||
overflow.close();
|
||||
}
|
||||
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
assertEquals(1, recovered.failedSize());
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(recovered));
|
||||
assertEquals(1, recovered.size());
|
||||
} finally {
|
||||
recovered.close();
|
||||
@@ -1372,32 +1378,32 @@ class BackendProxyHandlerLifecycleTest {
|
||||
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
AtomicReference<java.util.function.Consumer<OrderedVoteOutcome>> completion = new AtomicReference<>();
|
||||
try {
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
|
||||
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
|
||||
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
|
||||
setField(handler, "messageRouter", router);
|
||||
doAnswer(invocation -> {
|
||||
completion.set(invocation.getArgument(1));
|
||||
return null;
|
||||
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
|
||||
assertTrue(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
|
||||
.put("sequence", "active").build()));
|
||||
handler.activateInboundMessages();
|
||||
asyncTasks.removeFirst().run();
|
||||
|
||||
CompletableFuture.runAsync(handler::close).get(3, TimeUnit.SECONDS);
|
||||
long quarantineDeadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
|
||||
while (overflow.failedSize() != 1 && System.nanoTime() < quarantineDeadline) Thread.sleep(10L);
|
||||
assertEquals(1, overflow.failedSize());
|
||||
while (BackendOrderedVoteOverflowQueueAccess.failedSize(overflow) != 1 && System.nanoTime() < quarantineDeadline) Thread.sleep(10L);
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
|
||||
assertEquals(0, overflow.size());
|
||||
completion.get().accept(OrderedVoteOutcome.COMPLETE);
|
||||
assertEquals(1, overflow.failedSize());
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
|
||||
assertEquals(0, overflow.size());
|
||||
} finally {
|
||||
overflow.close();
|
||||
}
|
||||
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
|
||||
try {
|
||||
assertEquals(1, recovered.failedSize());
|
||||
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(recovered));
|
||||
assertEquals(0, recovered.size());
|
||||
} finally {
|
||||
recovered.close();
|
||||
@@ -1419,12 +1425,12 @@ class BackendProxyHandlerLifecycleTest {
|
||||
|
||||
JsonEnvelope first = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).put("sequence", "1").build();
|
||||
JsonEnvelope second = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).put("sequence", "2").build();
|
||||
previous.dispatchIncomingAfterPublication(first, mock(Runnable.class));
|
||||
previous.dispatchIncomingAfterPublication(second, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(previous, first, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(previous, second, mock(Runnable.class));
|
||||
|
||||
previous.completeOrderedVoteHandoff(replacement);
|
||||
JsonEnvelope late = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_ONLINE).put("sequence", "3").build();
|
||||
previous.dispatchIncomingAfterPublication(late, mock(Runnable.class));
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(previous, late, mock(Runnable.class));
|
||||
|
||||
ArrayDeque<Runnable> asyncTasks = new ArrayDeque<>();
|
||||
doAnswer(invocation -> {
|
||||
@@ -1498,7 +1504,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
|
||||
CompletableFuture<Void> callback = CompletableFuture.runAsync(() -> {
|
||||
started.countDown();
|
||||
handler.dispatchIncomingAfterPublication(envelope, localDispatch);
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, envelope, localDispatch);
|
||||
});
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
assertFalse(callback.isDone());
|
||||
@@ -1524,7 +1530,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
|
||||
CompletableFuture<Void> callback = CompletableFuture.runAsync(() -> {
|
||||
started.countDown();
|
||||
replacement.dispatchIncomingAfterPublication(envelope, replacementDispatch);
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(replacement, envelope, replacementDispatch);
|
||||
});
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
assertFalse(callback.isDone());
|
||||
@@ -1556,7 +1562,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
|
||||
CompletableFuture<Void> callback = CompletableFuture.runAsync(() -> {
|
||||
started.countDown();
|
||||
replacement.dispatchIncomingAfterPublication(envelope, replacementDispatch);
|
||||
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(replacement, envelope, replacementDispatch);
|
||||
});
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
assertFalse(callback.isDone());
|
||||
@@ -1608,7 +1614,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
try {
|
||||
Field transportField = BackendProxyTransportManager.class.getDeclaredField("transport");
|
||||
transportField.setAccessible(true);
|
||||
transportField.set(manager, previous);
|
||||
ReflectiveAccess.set(transportField, manager, previous);
|
||||
manager.restoreAfterFailedReplacement();
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new AssertionError(failure);
|
||||
@@ -1677,7 +1683,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
Field targetField = com.bencodez.votingplugin.VotingPluginMain.class
|
||||
.getDeclaredField("backendPluginMessageTarget");
|
||||
targetField.setAccessible(true);
|
||||
targetField.set(plugin, new AtomicReference<GlobalMessageHandler>());
|
||||
ReflectiveAccess.set(targetField, plugin, new AtomicReference<GlobalMessageHandler>());
|
||||
|
||||
plugin.activateBackendPluginMessageHandler(firstTarget);
|
||||
current.set(second);
|
||||
@@ -1864,7 +1870,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
BackendPresenceManager presence = mock(BackendPresenceManager.class);
|
||||
Field field = BackendProxyHandler.class.getDeclaredField("presenceManager");
|
||||
field.setAccessible(true);
|
||||
field.set(handler, presence);
|
||||
ReflectiveAccess.set(field, handler, presence);
|
||||
|
||||
verifyNoInteractions(presence);
|
||||
handler.activatePresenceReporting();
|
||||
@@ -1880,7 +1886,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
org.mockito.Mockito.doThrow(new java.util.concurrent.RejectedExecutionException()).when(presence).start();
|
||||
Field presenceField = BackendProxyHandler.class.getDeclaredField("presenceManager");
|
||||
presenceField.setAccessible(true);
|
||||
presenceField.set(handler, presence);
|
||||
ReflectiveAccess.set(presenceField, handler, presence);
|
||||
|
||||
Field managerField = BackendProxyHandler.class.getDeclaredField("transportManager");
|
||||
managerField.setAccessible(true);
|
||||
@@ -1888,7 +1894,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
BackendProxyTransport transport = mock(BackendProxyTransport.class);
|
||||
Field transportField = BackendProxyTransportManager.class.getDeclaredField("transport");
|
||||
transportField.setAccessible(true);
|
||||
transportField.set(manager, transport);
|
||||
ReflectiveAccess.set(transportField, manager, transport);
|
||||
|
||||
assertThrows(java.util.concurrent.RejectedExecutionException.class, handler::activatePresenceReporting);
|
||||
|
||||
@@ -1898,7 +1904,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
|
||||
@Test
|
||||
void sharesVoteDeduplicationAcrossHandlerReplacement() {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache();
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
|
||||
BackendProxyHandler previous = new BackendProxyHandler(null, cache);
|
||||
BackendProxyHandler replacement = new BackendProxyHandler(null, cache);
|
||||
|
||||
@@ -1917,7 +1923,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
BackendProxyTransport transport = mock(BackendProxyTransport.class);
|
||||
Field transportField = BackendProxyTransportManager.class.getDeclaredField("transport");
|
||||
transportField.setAccessible(true);
|
||||
transportField.set(manager, transport);
|
||||
ReflectiveAccess.set(transportField, manager, transport);
|
||||
|
||||
handler.prepareForReplacement(BungeeMethod.PLUGINMESSAGING);
|
||||
|
||||
@@ -1930,7 +1936,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
SocketHandler socket = mock(SocketHandler.class);
|
||||
Field field = SocketBackendProxyTransport.class.getDeclaredField("socketHandler");
|
||||
field.setAccessible(true);
|
||||
field.set(handler, socket);
|
||||
ReflectiveAccess.set(field, handler, socket);
|
||||
|
||||
handler.prepareForReplacement();
|
||||
|
||||
@@ -2017,10 +2023,10 @@ class BackendProxyHandlerLifecycleTest {
|
||||
RedisHandler redis = mock(RedisHandler.class);
|
||||
Field field = RedisBackendProxyTransport.class.getDeclaredField("redisHandler");
|
||||
field.setAccessible(true);
|
||||
field.set(transport, redis);
|
||||
ReflectiveAccess.set(field, transport, redis);
|
||||
Field transportField = BackendProxyTransportManager.class.getDeclaredField("transport");
|
||||
transportField.setAccessible(true);
|
||||
transportField.set(manager, transport);
|
||||
ReflectiveAccess.set(transportField, manager, transport);
|
||||
|
||||
handler.prepareForReplacement(BungeeMethod.REDIS);
|
||||
|
||||
@@ -2034,7 +2040,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
MqttHandler mqtt = mock(MqttHandler.class);
|
||||
Field field = MqttBackendProxyTransport.class.getDeclaredField("mqttHandler");
|
||||
field.setAccessible(true);
|
||||
field.set(handler, mqtt);
|
||||
ReflectiveAccess.set(field, handler, mqtt);
|
||||
|
||||
handler.prepareForReplacement();
|
||||
|
||||
@@ -2766,7 +2772,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
MySqlMessenger messenger = mock(MySqlMessenger.class);
|
||||
Field field = MysqlBackendProxyTransport.class.getDeclaredField("messenger");
|
||||
field.setAccessible(true);
|
||||
field.set(handler, messenger);
|
||||
ReflectiveAccess.set(field, handler, messenger);
|
||||
|
||||
handler.prepareForReplacement();
|
||||
|
||||
@@ -2911,7 +2917,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
ScheduledExecutorService timer = mock(ScheduledExecutorService.class);
|
||||
Field field = BackendGlobalDataSync.class.getDeclaredField("timer");
|
||||
field.setAccessible(true);
|
||||
field.set(handler, timer);
|
||||
ReflectiveAccess.set(field, handler, timer);
|
||||
|
||||
handler.close();
|
||||
|
||||
@@ -2944,7 +2950,7 @@ class BackendProxyHandlerLifecycleTest {
|
||||
try {
|
||||
Field field = type.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
ReflectiveAccess.set(field, target, value);
|
||||
return;
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
type = type.getSuperclass();
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.bencodez.votingplugin.tests.backendproxy.cache;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.backendproxy.cache.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Test-only forwarding access to the non-public production DurableVoteReceiptStore. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class DurableVoteReceiptStoreAccess implements ReflectiveAccess.View {
|
||||
public static final int MAX_ACTIVE_RECEIPTS = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", "MAX_ACTIVE_RECEIPTS");
|
||||
public static final int COMPLETION_HEADROOM = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", "COMPLETION_HEADROOM");
|
||||
public static final int MAX_RELEASE_TOMBSTONES = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", "MAX_RELEASE_TOMBSTONES");
|
||||
public static final long MAX_FILE_BYTES = (Long) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", "MAX_FILE_BYTES");
|
||||
public static final int MAX_RECORD_BYTES = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", "MAX_RECORD_BYTES");
|
||||
public static final int MAX_TOTAL_RECEIPTS = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", "MAX_TOTAL_RECEIPTS");
|
||||
private final Object delegate;
|
||||
private DurableVoteReceiptStoreAccess(Object delegate) { this.delegate = delegate; }
|
||||
public static DurableVoteReceiptStoreAccess view(Object delegate) { return delegate == null ? null : new DurableVoteReceiptStoreAccess(delegate); }
|
||||
@Override public Object delegate() { return delegate; }
|
||||
@Override public boolean equals(Object other) { return other instanceof DurableVoteReceiptStoreAccess value && delegate.equals(value.delegate); }
|
||||
@Override public int hashCode() { return delegate.hashCode(); }
|
||||
public static DurableVoteReceiptStoreAccess mock() { return view(org.mockito.Mockito.mock(ReflectiveAccess.type("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore"))); }
|
||||
public DurableVoteReceiptStoreAccess verify() { return view(org.mockito.Mockito.verify(delegate)); }
|
||||
public DurableVoteReceiptStoreAccess verify(org.mockito.verification.VerificationMode mode) { return view(org.mockito.Mockito.verify(delegate, mode)); }
|
||||
public DurableVoteReceiptStoreAccess stub(org.mockito.stubbing.Stubber stubber) { return view(stubber.when(delegate)); }
|
||||
public DurableVoteReceiptStoreAccess(Path file) throws IOException { this(ReflectiveAccess.construct("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", new String[]{"Path"}, new Object[]{file})); }
|
||||
public DurableVoteReceiptStoreAccess(Path file, int maxActiveReceipts, int completionHeadroom, int maxReleaseTombstones) throws IOException { this(ReflectiveAccess.construct("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", new String[]{"Path", "int", "int", "int"}, new Object[]{file, maxActiveReceipts, completionHeadroom, maxReleaseTombstones})); }
|
||||
public DurableVoteReceiptStoreAccess(Path file, int maxActiveReceipts, int completionHeadroom, int maxReleaseTombstones, int maxTotalReceipts) throws IOException { this(ReflectiveAccess.construct("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", new String[]{"Path", "int", "int", "int", "int"}, new Object[]{file, maxActiveReceipts, completionHeadroom, maxReleaseTombstones, maxTotalReceipts})); }
|
||||
public Map<UUID, Long> snapshot() { return (Map<UUID, Long>) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", delegate, "snapshot", new String[]{}, new Object[]{}); }
|
||||
public boolean contains(UUID voteId) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", delegate, "contains", new String[]{"UUID"}, new Object[]{voteId}); }
|
||||
public long complete(UUID voteId) { return (Long) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", delegate, "complete", new String[]{"UUID"}, new Object[]{voteId}); }
|
||||
public long release(UUID voteId) { return (Long) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.cache.DurableVoteReceiptStore", delegate, "release", new String[]{"UUID"}, new Object[]{voteId}); }
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.bencodez.votingplugin.tests.backendproxy.cache;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.backendproxy.cache.*;
|
||||
import com.bencodez.votingplugin.backendproxy.cache.ProcessedVoteCache.*;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/** Typed test-only access to package-private ProcessedVoteCache seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class ProcessedVoteCacheAccess {
|
||||
public static final int DEFAULT_MAX_TRACKED_VOTES = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.cache.ProcessedVoteCache", "DEFAULT_MAX_TRACKED_VOTES");
|
||||
private ProcessedVoteCacheAccess() { }
|
||||
public static ProcessedVoteCache create() { return (ProcessedVoteCache) ReflectiveAccess.construct("com.bencodez.votingplugin.backendproxy.cache.ProcessedVoteCache", new String[]{}, new Object[]{}); }
|
||||
public static ProcessedVoteCache create(long ttlMillis) { return (ProcessedVoteCache) ReflectiveAccess.construct("com.bencodez.votingplugin.backendproxy.cache.ProcessedVoteCache", new String[]{"long"}, new Object[]{ttlMillis}); }
|
||||
public static ProcessedVoteCache create(Path receiptFile) { return (ProcessedVoteCache) ReflectiveAccess.construct("com.bencodez.votingplugin.backendproxy.cache.ProcessedVoteCache", new String[]{"Path"}, new Object[]{receiptFile}); }
|
||||
public static ProcessedVoteCache create(long ttlMillis, Path receiptFile) { return (ProcessedVoteCache) ReflectiveAccess.construct("com.bencodez.votingplugin.backendproxy.cache.ProcessedVoteCache", new String[]{"long", "Path"}, new Object[]{ttlMillis, receiptFile}); }
|
||||
public static ProcessedVoteCache create(long ttlMillis, int maxTrackedVotes) { return (ProcessedVoteCache) ReflectiveAccess.construct("com.bencodez.votingplugin.backendproxy.cache.ProcessedVoteCache", new String[]{"long", "int"}, new Object[]{ttlMillis, maxTrackedVotes}); }
|
||||
public static ProcessedVoteCache create(long ttlMillis, int maxTrackedVotes, DurableVoteReceiptStoreAccess durableReceipts) { return (ProcessedVoteCache) ReflectiveAccess.construct("com.bencodez.votingplugin.backendproxy.cache.ProcessedVoteCache", new String[]{"long", "int", "DurableVoteReceiptStore"}, new Object[]{ttlMillis, maxTrackedVotes, durableReceipts}); }
|
||||
}
|
||||
+40
-37
@@ -1,4 +1,7 @@
|
||||
package com.bencodez.votingplugin.backendproxy.cache;
|
||||
package com.bencodez.votingplugin.tests.backendproxy.cache;
|
||||
|
||||
import com.bencodez.votingplugin.tests.backendproxy.cache.DurableVoteReceiptStoreAccess;
|
||||
import com.bencodez.votingplugin.backendproxy.cache.ProcessedVoteCache;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -16,7 +19,7 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
|
||||
@Test
|
||||
void duplicateRetryDoesNotScanAndPruneUnrelatedEntries() {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache(TimeUnit.MINUTES.toMillis(30), 2);
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(TimeUnit.MINUTES.toMillis(30), 2);
|
||||
UUID expired = UUID.randomUUID();
|
||||
UUID live = UUID.randomUUID();
|
||||
cache.getProcessedVotes().put(expired, 0L);
|
||||
@@ -29,7 +32,7 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
|
||||
@Test
|
||||
void expiredHeadRetryReclaimsItsSlotBeforeSaturationCheck() {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache(TimeUnit.MINUTES.toMillis(30), 1);
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(TimeUnit.MINUTES.toMillis(30), 1);
|
||||
UUID expired = UUID.randomUUID();
|
||||
cache.getProcessedVotes().put(expired, 0L);
|
||||
|
||||
@@ -44,44 +47,44 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
void releaseTombstoneCapacityCoversSustainedVoteThroughputForFullTtl() {
|
||||
long threeVotesPerSecondForOneDay = TimeUnit.DAYS.toSeconds(1) * 3;
|
||||
|
||||
assertTrue(DurableVoteReceiptStore.MAX_RELEASE_TOMBSTONES >= threeVotesPerSecondForOneDay);
|
||||
assertTrue(DurableVoteReceiptStoreAccess.MAX_RELEASE_TOMBSTONES >= threeVotesPerSecondForOneDay);
|
||||
}
|
||||
|
||||
@Test
|
||||
void completedVoteRemainsDeduplicatedAfterBackendRestart() {
|
||||
Path receipts = directory.resolve("receipts.dat");
|
||||
UUID voteId = UUID.randomUUID();
|
||||
ProcessedVoteCache first = new ProcessedVoteCache(receipts);
|
||||
ProcessedVoteCache first = ProcessedVoteCacheAccess.create(receipts);
|
||||
|
||||
assertTrue(first.reserve(voteId));
|
||||
assertTrue(first.complete(voteId));
|
||||
assertTrue(first.hasDurableReceipt(voteId));
|
||||
assertFalse(new ProcessedVoteCache(receipts).reserve(voteId));
|
||||
assertFalse(ProcessedVoteCacheAccess.create(receipts).reserve(voteId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reservationWithoutCompletionDoesNotSuppressRestartRetry() {
|
||||
Path receipts = directory.resolve("receipts.dat");
|
||||
UUID voteId = UUID.randomUUID();
|
||||
assertTrue(new ProcessedVoteCache(receipts).reserve(voteId));
|
||||
assertTrue(ProcessedVoteCacheAccess.create(receipts).reserve(voteId));
|
||||
|
||||
assertTrue(new ProcessedVoteCache(receipts).reserve(voteId));
|
||||
assertTrue(ProcessedVoteCacheAccess.create(receipts).reserve(voteId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void repairsTruncatedTailBeforeAppendingAnotherReceipt() throws Exception {
|
||||
Path receipts = directory.resolve("receipts.dat");
|
||||
UUID firstId = UUID.randomUUID();
|
||||
ProcessedVoteCache first = new ProcessedVoteCache(receipts);
|
||||
ProcessedVoteCache first = ProcessedVoteCacheAccess.create(receipts);
|
||||
assertTrue(first.reserve(firstId));
|
||||
assertTrue(first.complete(firstId));
|
||||
Files.writeString(receipts, "truncated", StandardOpenOption.APPEND);
|
||||
|
||||
ProcessedVoteCache repaired = new ProcessedVoteCache(receipts);
|
||||
ProcessedVoteCache repaired = ProcessedVoteCacheAccess.create(receipts);
|
||||
UUID secondId = UUID.randomUUID();
|
||||
assertTrue(repaired.reserve(secondId));
|
||||
assertTrue(repaired.complete(secondId));
|
||||
ProcessedVoteCache restarted = new ProcessedVoteCache(receipts);
|
||||
ProcessedVoteCache restarted = ProcessedVoteCacheAccess.create(receipts);
|
||||
assertFalse(restarted.reserve(firstId));
|
||||
assertFalse(restarted.reserve(secondId));
|
||||
}
|
||||
@@ -91,14 +94,14 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
Path receipts = directory.resolve("receipts.dat");
|
||||
UUID firstId = UUID.randomUUID();
|
||||
UUID secondId = UUID.randomUUID();
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache(receipts);
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(receipts);
|
||||
assertTrue(cache.reserve(firstId));
|
||||
assertTrue(cache.complete(firstId));
|
||||
Files.writeString(receipts, "partial", StandardOpenOption.APPEND);
|
||||
|
||||
assertTrue(cache.reserve(secondId));
|
||||
assertTrue(cache.complete(secondId));
|
||||
ProcessedVoteCache restarted = new ProcessedVoteCache(receipts);
|
||||
ProcessedVoteCache restarted = ProcessedVoteCacheAccess.create(receipts);
|
||||
assertFalse(restarted.reserve(firstId));
|
||||
assertFalse(restarted.reserve(secondId));
|
||||
}
|
||||
@@ -109,7 +112,7 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
UUID incompleteId = UUID.randomUUID();
|
||||
Files.writeString(receipts, "VP-VOTE-RECEIPTS-1\n" + incompleteId + "\t9");
|
||||
|
||||
ProcessedVoteCache repaired = new ProcessedVoteCache(receipts);
|
||||
ProcessedVoteCache repaired = ProcessedVoteCacheAccess.create(receipts);
|
||||
|
||||
assertTrue(repaired.reserve(incompleteId));
|
||||
assertTrue(Files.readString(receipts).endsWith("\n"));
|
||||
@@ -120,7 +123,7 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
Path unusableParent = directory.resolve("not-a-directory");
|
||||
Files.writeString(unusableParent, "file");
|
||||
UUID voteId = UUID.randomUUID();
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache(1L, unusableParent.resolve("receipts.dat"));
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(1L, unusableParent.resolve("receipts.dat"));
|
||||
|
||||
assertTrue(cache.reserve(voteId));
|
||||
assertFalse(cache.complete(voteId));
|
||||
@@ -132,31 +135,31 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
void proxyConfirmedReleaseLeavesRestartSafeTombstone() {
|
||||
Path receipts = directory.resolve("receipts.dat");
|
||||
UUID voteId = UUID.randomUUID();
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache(receipts);
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(receipts);
|
||||
assertTrue(cache.reserve(voteId));
|
||||
assertTrue(cache.complete(voteId));
|
||||
|
||||
assertTrue(cache.releaseCompletedReceipt(voteId));
|
||||
|
||||
assertFalse(new ProcessedVoteCache(receipts).reserve(voteId));
|
||||
assertFalse(ProcessedVoteCacheAccess.create(receipts).reserve(voteId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownReceiptReleaseLeavesRestartSafeTombstone() {
|
||||
Path receipts = directory.resolve("receipts.dat");
|
||||
UUID voteId = UUID.randomUUID();
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache(receipts);
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(receipts);
|
||||
|
||||
assertFalse(cache.hasDurableReceipt(voteId));
|
||||
assertTrue(cache.releaseCompletedReceipt(voteId));
|
||||
assertTrue(cache.hasDurableReceipt(voteId));
|
||||
|
||||
assertFalse(new ProcessedVoteCache(receipts).reserve(voteId));
|
||||
assertFalse(ProcessedVoteCacheAccess.create(receipts).reserve(voteId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionHeadroomAllowsPrecedingVotesBeforeRelease() throws Exception {
|
||||
DurableVoteReceiptStore store = new DurableVoteReceiptStore(directory.resolve("receipts.dat"), 1, 2, 1);
|
||||
DurableVoteReceiptStoreAccess store = new DurableVoteReceiptStoreAccess(directory.resolve("receipts.dat"), 1, 2, 1);
|
||||
UUID first = UUID.randomUUID();
|
||||
UUID second = UUID.randomUUID();
|
||||
UUID third = UUID.randomUUID();
|
||||
@@ -173,14 +176,14 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
@Test
|
||||
void activeReceiptWaitsWhenTombstoneCapacityIsFull() throws Exception {
|
||||
Path receipts = directory.resolve("receipts.dat");
|
||||
DurableVoteReceiptStore store = new DurableVoteReceiptStore(receipts, 1, 0, 1);
|
||||
DurableVoteReceiptStoreAccess store = new DurableVoteReceiptStoreAccess(receipts, 1, 0, 1);
|
||||
UUID active = UUID.randomUUID();
|
||||
assertTrue(store.complete(active) > 0L);
|
||||
assertTrue(store.release(UUID.randomUUID()) > 0L);
|
||||
|
||||
assertFalse(store.release(active) > 0L);
|
||||
|
||||
new DurableVoteReceiptStore(receipts, 1, 0, 1);
|
||||
new DurableVoteReceiptStoreAccess(receipts, 1, 0, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -189,12 +192,12 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
UUID voteId = UUID.randomUUID();
|
||||
Files.writeString(receipts, "VP-VOTE-RECEIPTS-1\nR\t" + voteId + "\t1\n");
|
||||
|
||||
assertTrue(new ProcessedVoteCache(receipts).reserve(voteId));
|
||||
assertTrue(ProcessedVoteCacheAccess.create(receipts).reserve(voteId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void saturationDoesNotEvictLiveReplayFences() {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache(TimeUnit.MINUTES.toMillis(30), 2);
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(TimeUnit.MINUTES.toMillis(30), 2);
|
||||
UUID first = UUID.randomUUID();
|
||||
UUID second = UUID.randomUUID();
|
||||
|
||||
@@ -208,19 +211,19 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
|
||||
@Test
|
||||
void defaultCapacityIncludesDurableCompletionHeadroom() {
|
||||
assertTrue(ProcessedVoteCache.DEFAULT_MAX_TRACKED_VOTES
|
||||
== DurableVoteReceiptStore.MAX_ACTIVE_RECEIPTS + DurableVoteReceiptStore.COMPLETION_HEADROOM);
|
||||
assertTrue(ProcessedVoteCache.DEFAULT_MAX_TRACKED_VOTES <= DurableVoteReceiptStore.MAX_TOTAL_RECEIPTS);
|
||||
assertTrue((long) DurableVoteReceiptStore.MAX_TOTAL_RECEIPTS * DurableVoteReceiptStore.MAX_RECORD_BYTES
|
||||
+ "VP-VOTE-RECEIPTS-1\n".length() <= DurableVoteReceiptStore.MAX_FILE_BYTES);
|
||||
assertTrue((long) (DurableVoteReceiptStore.MAX_TOTAL_RECEIPTS + 1)
|
||||
* DurableVoteReceiptStore.MAX_RECORD_BYTES + "VP-VOTE-RECEIPTS-1\n".length()
|
||||
> DurableVoteReceiptStore.MAX_FILE_BYTES);
|
||||
assertTrue(ProcessedVoteCacheAccess.DEFAULT_MAX_TRACKED_VOTES
|
||||
== DurableVoteReceiptStoreAccess.MAX_ACTIVE_RECEIPTS + DurableVoteReceiptStoreAccess.COMPLETION_HEADROOM);
|
||||
assertTrue(ProcessedVoteCacheAccess.DEFAULT_MAX_TRACKED_VOTES <= DurableVoteReceiptStoreAccess.MAX_TOTAL_RECEIPTS);
|
||||
assertTrue((long) DurableVoteReceiptStoreAccess.MAX_TOTAL_RECEIPTS * DurableVoteReceiptStoreAccess.MAX_RECORD_BYTES
|
||||
+ "VP-VOTE-RECEIPTS-1\n".length() <= DurableVoteReceiptStoreAccess.MAX_FILE_BYTES);
|
||||
assertTrue((long) (DurableVoteReceiptStoreAccess.MAX_TOTAL_RECEIPTS + 1)
|
||||
* DurableVoteReceiptStoreAccess.MAX_RECORD_BYTES + "VP-VOTE-RECEIPTS-1\n".length()
|
||||
> DurableVoteReceiptStoreAccess.MAX_FILE_BYTES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixedActiveAndReleaseReceiptsRespectTheJournalByteAlignedLimit() throws Exception {
|
||||
DurableVoteReceiptStore store = new DurableVoteReceiptStore(directory.resolve("mixed-receipts.dat"), 2, 0, 2, 2);
|
||||
DurableVoteReceiptStoreAccess store = new DurableVoteReceiptStoreAccess(directory.resolve("mixed-receipts.dat"), 2, 0, 2, 2);
|
||||
UUID released = UUID.randomUUID();
|
||||
UUID active = UUID.randomUUID();
|
||||
|
||||
@@ -244,7 +247,7 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
+ "R\t" + firstReleased + "\t" + expiresAt + "\n"
|
||||
+ "R\t" + secondReleased + "\t" + expiresAt + "\n");
|
||||
|
||||
DurableVoteReceiptStore store = new DurableVoteReceiptStore(receipts, 2, 0, 2, 2);
|
||||
DurableVoteReceiptStoreAccess store = new DurableVoteReceiptStoreAccess(receipts, 2, 0, 2, 2);
|
||||
|
||||
assertTrue(store.contains(firstActive));
|
||||
assertTrue(store.contains(secondActive));
|
||||
@@ -255,11 +258,11 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
|
||||
@Test
|
||||
void mixedJournalUsesDurablePerKindCapacityForNewAdmission() throws Exception {
|
||||
DurableVoteReceiptStore store = new DurableVoteReceiptStore(directory.resolve("mixed-capacity.dat"), 2, 1, 2, 4);
|
||||
DurableVoteReceiptStoreAccess store = new DurableVoteReceiptStoreAccess(directory.resolve("mixed-capacity.dat"), 2, 1, 2, 4);
|
||||
assertTrue(store.release(UUID.randomUUID()) > 0L);
|
||||
assertTrue(store.release(UUID.randomUUID()) > 0L);
|
||||
assertTrue(store.complete(UUID.randomUUID()) > 0L);
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache(TimeUnit.MINUTES.toMillis(30), 2, store);
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(TimeUnit.MINUTES.toMillis(30), 2, store);
|
||||
UUID admitted = UUID.randomUUID();
|
||||
UUID stale = UUID.randomUUID();
|
||||
cache.getProcessedVotes().put(stale, 0L);
|
||||
@@ -273,7 +276,7 @@ class ProcessedVoteCacheDurabilityTest {
|
||||
|
||||
@Test
|
||||
void cancelledValidationReservationFreesCapacity() {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache(TimeUnit.MINUTES.toMillis(30), 1);
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(TimeUnit.MINUTES.toMillis(30), 1);
|
||||
UUID invalid = UUID.randomUUID();
|
||||
|
||||
assertTrue(cache.reserve(invalid));
|
||||
+7
-7
@@ -16,7 +16,7 @@ public class ProcessedVoteCacheTest {
|
||||
|
||||
@Test
|
||||
public void duplicateVoteIdIsRejected() {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache();
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
|
||||
UUID voteId = UUID.randomUUID();
|
||||
assertTrue(cache.reserve(voteId));
|
||||
assertFalse(cache.reserve(voteId));
|
||||
@@ -25,7 +25,7 @@ public class ProcessedVoteCacheTest {
|
||||
|
||||
@Test
|
||||
public void nullVoteIdsRemainCompatible() {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache();
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
|
||||
assertTrue(cache.reserve(null));
|
||||
assertTrue(cache.reserve(null));
|
||||
assertTrue(cache.getProcessedVotes().isEmpty());
|
||||
@@ -33,7 +33,7 @@ public class ProcessedVoteCacheTest {
|
||||
|
||||
@Test
|
||||
public void expiredVoteIdCanBeReservedAgain() throws Exception {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache(1);
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(1);
|
||||
UUID voteId = UUID.randomUUID();
|
||||
assertTrue(cache.reserve(voteId));
|
||||
Thread.sleep(5);
|
||||
@@ -43,7 +43,7 @@ public class ProcessedVoteCacheTest {
|
||||
|
||||
@Test
|
||||
public void redisDeliveryIsAcceptedOnceAcrossSharedHandlers() {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache();
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
|
||||
String deliveryId = UUID.randomUUID().toString();
|
||||
assertTrue(cache.reserveRedisDelivery(deliveryId));
|
||||
assertFalse(cache.reserveRedisDelivery(deliveryId));
|
||||
@@ -53,7 +53,7 @@ public class ProcessedVoteCacheTest {
|
||||
|
||||
@Test
|
||||
public void legacyRedisHandoffMatchesOldAndBufferedDeliveryCounts() {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache();
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
|
||||
Object previous = new Object();
|
||||
Object replacement = new Object();
|
||||
cache.registerRedisSubscriber(previous);
|
||||
@@ -69,7 +69,7 @@ public class ProcessedVoteCacheTest {
|
||||
|
||||
@Test
|
||||
public void legacyRedisHandoffAccountingIsBounded() throws Exception {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache();
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
|
||||
Object previous = new Object();
|
||||
Object replacement = new Object();
|
||||
cache.registerRedisSubscriber(previous);
|
||||
@@ -87,7 +87,7 @@ public class ProcessedVoteCacheTest {
|
||||
|
||||
@Test
|
||||
public void legacyRedisHandoffAccountingRejectsOversizedSignatures() throws Exception {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache();
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
|
||||
Object previous = new Object();
|
||||
cache.registerRedisSubscriber(previous);
|
||||
cache.registerRedisSubscriber(new Object());
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.bencodez.votingplugin.tests.backendproxy.global;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.backendproxy.global.*;
|
||||
import com.bencodez.votingplugin.backendproxy.global.BackendGlobalDataSync.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Typed test-only access to package-private BackendGlobalDataSync seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class BackendGlobalDataSyncAccess {
|
||||
public static void updateLastOnline(BackendGlobalDataSync target) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.global.BackendGlobalDataSync", target, "updateLastOnline", new String[]{}, new Object[]{}); }
|
||||
private BackendGlobalDataSyncAccess() { }
|
||||
public static void close(BackendGlobalDataSync target) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.global.BackendGlobalDataSync", target, "close", new String[]{}, new Object[]{}); }
|
||||
public static void close(BackendGlobalDataSync target, long timeout, TimeUnit unit) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.global.BackendGlobalDataSync", target, "close", new String[]{"long", "TimeUnit"}, new Object[]{timeout, unit}); }
|
||||
|
||||
}
|
||||
+15
-11
@@ -1,4 +1,8 @@
|
||||
package com.bencodez.votingplugin.backendproxy.global;
|
||||
package com.bencodez.votingplugin.tests.backendproxy.global;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import com.bencodez.votingplugin.backendproxy.global.BackendGlobalDataSync;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -287,7 +291,7 @@ class BackendGlobalDataSyncTest {
|
||||
oldSync.checkGlobalData();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, schedulerRuns.get());
|
||||
oldSync.handoffCompletionSender(replacement);
|
||||
oldSync.close(1, TimeUnit.MILLISECONDS);
|
||||
BackendGlobalDataSyncAccess.close(oldSync, 1, TimeUnit.MILLISECONDS);
|
||||
|
||||
replacement.checkGlobalData();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, schedulerRuns.get());
|
||||
@@ -400,7 +404,7 @@ class BackendGlobalDataSyncTest {
|
||||
CompletableFuture<Void> execution = CompletableFuture.runAsync(asyncWrite.get());
|
||||
assertTrue(writeStarted.await(1, TimeUnit.SECONDS));
|
||||
oldSync.handoffCompletionSender(replacement);
|
||||
oldSync.close(1, TimeUnit.MILLISECONDS);
|
||||
BackendGlobalDataSyncAccess.close(oldSync, 1, TimeUnit.MILLISECONDS);
|
||||
|
||||
replacement.checkGlobalData();
|
||||
verify(replacementHandler).setBoolean("lobby", "ForceUpdate", false);
|
||||
@@ -551,7 +555,7 @@ class BackendGlobalDataSyncTest {
|
||||
setField(sync, "globalDataHandler", handler);
|
||||
setField(sync, "ownsGlobalMysql", false);
|
||||
|
||||
CompletableFuture<Void> heartbeat = CompletableFuture.runAsync(sync::updateLastOnline);
|
||||
CompletableFuture<Void> heartbeat = CompletableFuture.runAsync(() -> BackendGlobalDataSyncAccess.updateLastOnline(sync));
|
||||
assertTrue(heartbeatStarted.await(1, TimeUnit.SECONDS));
|
||||
CompletableFuture<Void> close = CompletableFuture.runAsync(sync::close);
|
||||
Thread.sleep(50L);
|
||||
@@ -961,7 +965,7 @@ class BackendGlobalDataSyncTest {
|
||||
assertTrue(oldSync.checkGlobalDataTime(TimeType.DAY, data));
|
||||
scheduled.get().run();
|
||||
oldSync.handoffCompletionSender(replacement);
|
||||
oldSync.close(1, TimeUnit.MILLISECONDS);
|
||||
BackendGlobalDataSyncAccess.close(oldSync, 1, TimeUnit.MILLISECONDS);
|
||||
replacement.checkGlobalData();
|
||||
|
||||
verify(timeChecker).forceChanged(TimeType.DAY, false, true, true);
|
||||
@@ -1083,7 +1087,7 @@ class BackendGlobalDataSyncTest {
|
||||
week.put(TimeType.WEEK.toString(), new DataValueBoolean(true));
|
||||
|
||||
assertTrue(oldSync.checkGlobalDataTime(TimeType.DAY, day));
|
||||
oldSync.close(1, TimeUnit.MILLISECONDS);
|
||||
BackendGlobalDataSyncAccess.close(oldSync, 1, TimeUnit.MILLISECONDS);
|
||||
assertFalse(replacement.checkGlobalDataTime(TimeType.DAY, day));
|
||||
assertTrue(replacement.checkGlobalDataTime(TimeType.WEEK, week));
|
||||
org.junit.jupiter.api.Assertions.assertEquals(2, scheduled.size());
|
||||
@@ -1124,7 +1128,7 @@ class BackendGlobalDataSyncTest {
|
||||
week.put(TimeType.WEEK.toString(), new DataValueBoolean(true));
|
||||
|
||||
assertTrue(oldSync.checkGlobalDataTime(TimeType.DAY, day));
|
||||
oldSync.close(1, TimeUnit.MILLISECONDS);
|
||||
BackendGlobalDataSyncAccess.close(oldSync, 1, TimeUnit.MILLISECONDS);
|
||||
BackendGlobalDataSync replacement = new BackendGlobalDataSync(plugin, ignored -> { });
|
||||
setField(replacement, "globalDataHandler", replacementHandler);
|
||||
|
||||
@@ -1219,7 +1223,7 @@ class BackendGlobalDataSyncTest {
|
||||
() -> sync.checkGlobalDataTime(TimeType.DAY, data));
|
||||
assertTrue(writeStarted.await(1, TimeUnit.SECONDS));
|
||||
|
||||
sync.close(50L, TimeUnit.MILLISECONDS);
|
||||
BackendGlobalDataSyncAccess.close(sync, 50L, TimeUnit.MILLISECONDS);
|
||||
verify(mysql).close();
|
||||
|
||||
releaseWrite.countDown();
|
||||
@@ -1239,7 +1243,7 @@ class BackendGlobalDataSyncTest {
|
||||
setField(sync, "globalDataHandler", handler);
|
||||
setField(sync, "ownsGlobalMysql", false);
|
||||
|
||||
sync.close();
|
||||
BackendGlobalDataSyncAccess.close(sync);
|
||||
|
||||
verify(mysql, never()).close();
|
||||
}
|
||||
@@ -1254,7 +1258,7 @@ class BackendGlobalDataSyncTest {
|
||||
setField(sync, "globalDataHandler", handler);
|
||||
setField(sync, "ownsGlobalMysql", true);
|
||||
|
||||
sync.close();
|
||||
BackendGlobalDataSyncAccess.close(sync);
|
||||
|
||||
verify(mysql).close();
|
||||
}
|
||||
@@ -1263,7 +1267,7 @@ class BackendGlobalDataSyncTest {
|
||||
try {
|
||||
Field field = BackendGlobalDataSync.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
ReflectiveAccess.set(field, target, value);
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new AssertionError(failure);
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.bencodez.votingplugin.tests.backendproxy.messaging;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.backendproxy.messaging.*;
|
||||
import com.bencodez.votingplugin.backendproxy.messaging.BackendProxyMessageRouter.*;
|
||||
import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope;
|
||||
|
||||
/** Typed test-only access to package-private BackendProxyMessageRouter seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class BackendProxyMessageRouterAccess {
|
||||
private BackendProxyMessageRouterAccess() { }
|
||||
public static void handleVoteUpdate(BackendProxyMessageRouter target, JsonEnvelope msg) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.messaging.BackendProxyMessageRouter", target, "handleVoteUpdate", new String[]{"JsonEnvelope"}, new Object[]{msg}); }
|
||||
public static void handleVoteUpdate(BackendProxyMessageRouter target, JsonEnvelope msg, Runnable completion) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.messaging.BackendProxyMessageRouter", target, "handleVoteUpdate", new String[]{"JsonEnvelope", "Runnable"}, new Object[]{msg, completion}); }
|
||||
}
|
||||
+14
-12
@@ -1,4 +1,6 @@
|
||||
package com.bencodez.votingplugin.backendproxy.messaging;
|
||||
package com.bencodez.votingplugin.tests.backendproxy.messaging;
|
||||
|
||||
import com.bencodez.votingplugin.backendproxy.messaging.BackendProxyMessageRouter;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -114,7 +116,7 @@ class BackendProxyMessageRouterTest {
|
||||
|
||||
@Test
|
||||
void ignoresLastVoteTimeForUnknownServiceSite() {
|
||||
router.handleVoteUpdate(VotingPluginWire.voteUpdate(PLAYER_UUID.toString(), 1, 10,
|
||||
BackendProxyMessageRouterAccess.handleVoteUpdate(router, VotingPluginWire.voteUpdate(PLAYER_UUID.toString(), 1, 10,
|
||||
"unknown.example\nforged-entry", LAST_VOTE_TIME, ""));
|
||||
|
||||
verify(user, times(2)).cache();
|
||||
@@ -130,7 +132,7 @@ class BackendProxyMessageRouterTest {
|
||||
VoteSite voteSite = mock(VoteSite.class);
|
||||
when(voteSiteManager.getVoteSite("known.example", true)).thenReturn(voteSite);
|
||||
|
||||
router.handleVoteUpdate(VotingPluginWire.voteUpdate(PLAYER_UUID.toString(), 1, 10,
|
||||
BackendProxyMessageRouterAccess.handleVoteUpdate(router, VotingPluginWire.voteUpdate(PLAYER_UUID.toString(), 1, 10,
|
||||
"known.example", LAST_VOTE_TIME, ""));
|
||||
|
||||
verify(user, times(2)).cache();
|
||||
@@ -787,7 +789,7 @@ class BackendProxyMessageRouterTest {
|
||||
}).when(coreUserManager).getUserAsync(eq(PLAYER_UUID), any(), any());
|
||||
|
||||
AtomicInteger completions = new AtomicInteger();
|
||||
router.handleVoteUpdate(VotingPluginWire.voteUpdate(PLAYER_UUID.toString(), 1, 10,
|
||||
BackendProxyMessageRouterAccess.handleVoteUpdate(router, VotingPluginWire.voteUpdate(PLAYER_UUID.toString(), 1, 10,
|
||||
"known.example", LAST_VOTE_TIME, ""), completions::incrementAndGet);
|
||||
|
||||
assertEquals(1, completions.get());
|
||||
@@ -855,7 +857,7 @@ class BackendProxyMessageRouterTest {
|
||||
var field = VotingPluginUser.class.getDeclaredField("plugin"); field.setAccessible(true); field.set(user, plugin);
|
||||
org.mockito.Mockito.doCallRealMethod().when(user).bungeeVotePluginMessagingEvent(
|
||||
any(), anyLong(), any(), anyBoolean(), anyBoolean(), anyBoolean(), anyInt(), anyBoolean(),
|
||||
anyBoolean(), anyBoolean(), any(), anyBoolean());
|
||||
anyBoolean(), anyBoolean(), any(), anyBoolean(), anyBoolean(), anyBoolean());
|
||||
var server = mock(org.bukkit.Server.class);
|
||||
var manager = mock(org.bukkit.plugin.PluginManager.class);
|
||||
when(plugin.getServer()).thenReturn(server); when(server.getPluginManager()).thenReturn(manager);
|
||||
@@ -895,7 +897,7 @@ class BackendProxyMessageRouterTest {
|
||||
.thenReturn(user);
|
||||
doThrow(new IllegalStateException("partial reward")).when(user).bungeeVotePluginMessagingEvent(
|
||||
any(), anyLong(), any(), anyBoolean(), anyBoolean(), anyBoolean(), anyInt(), anyBoolean(), anyBoolean(),
|
||||
anyBoolean(), any(), anyBoolean());
|
||||
anyBoolean(), any(), anyBoolean(), eq(true), anyBoolean());
|
||||
BackendProxyMessageRouter voteRouter = new BackendProxyMessageRouter(plugin,
|
||||
mock(BackendPresenceManager.class), mock(BackendGlobalDataSync.class),
|
||||
mock(BackendVotePartySync.class), cache);
|
||||
@@ -941,7 +943,7 @@ class BackendProxyMessageRouterTest {
|
||||
assertEquals(OrderedVoteOutcome.COMPLETE, outcome.get());
|
||||
verify(cache, times(2)).complete(voteId);
|
||||
verify(user, times(1)).bungeeVotePluginMessagingEvent(any(), anyLong(), any(), anyBoolean(), anyBoolean(),
|
||||
anyBoolean(), anyInt(), eq(false), eq(true), eq(true), eq(voteId), eq(false));
|
||||
anyBoolean(), anyInt(), eq(false), eq(true), eq(true), eq(voteId), eq(false), eq(true), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -962,7 +964,7 @@ class BackendProxyMessageRouterTest {
|
||||
LAST_VOTE_TIME, true, true, "", voteId, false, false, 1, 1), ignored -> { });
|
||||
|
||||
verify(user).bungeeVotePluginMessagingEvent(any(), anyLong(), any(), anyBoolean(), anyBoolean(),
|
||||
anyBoolean(), anyInt(), eq(false), eq(false), eq(true), eq(voteId), eq(false));
|
||||
anyBoolean(), anyInt(), eq(false), eq(false), eq(true), eq(voteId), eq(false), eq(true), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -982,7 +984,7 @@ class BackendProxyMessageRouterTest {
|
||||
LAST_VOTE_TIME, true, true, "", voteId, false, false, 1, 1), ignored -> { });
|
||||
|
||||
verify(user).bungeeVotePluginMessagingEvent(any(), anyLong(), any(), anyBoolean(), anyBoolean(),
|
||||
anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), anyBoolean(), eq(voteId), eq(true));
|
||||
anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), anyBoolean(), eq(voteId), eq(true), eq(true), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1004,7 +1006,7 @@ class BackendProxyMessageRouterTest {
|
||||
assertEquals(OrderedVoteOutcome.QUARANTINE, outcome.get());
|
||||
verify(cache, never()).complete(voteId);
|
||||
verify(user, never()).bungeeVotePluginMessagingEvent(any(), anyLong(), any(), anyBoolean(), anyBoolean(),
|
||||
anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), anyBoolean(), any(), anyBoolean());
|
||||
anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), anyBoolean(), any(), anyBoolean(), eq(true), anyBoolean());
|
||||
verify(cache).cancelReservation(voteId);
|
||||
}
|
||||
|
||||
@@ -1024,7 +1026,7 @@ class BackendProxyMessageRouterTest {
|
||||
|
||||
assertEquals(OrderedVoteOutcome.RETRY, outcome.get());
|
||||
verify(user, never()).bungeeVotePluginMessagingEvent(any(), anyLong(), any(), anyBoolean(), anyBoolean(),
|
||||
anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), anyBoolean(), any(), anyBoolean());
|
||||
anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), anyBoolean(), any(), anyBoolean(), eq(true), anyBoolean());
|
||||
verify(cache, never()).complete(voteId);
|
||||
}
|
||||
|
||||
@@ -1247,7 +1249,7 @@ class BackendProxyMessageRouterTest {
|
||||
voteRouter.handleOrderedVote(queued, ignored -> { });
|
||||
|
||||
verify(user).bungeeVotePluginMessagingEvent(any(), anyLong(), any(), anyBoolean(), anyBoolean(),
|
||||
anyBoolean(), anyInt(), eq(true), eq(true), eq(true), eq(voteId), eq(true));
|
||||
anyBoolean(), anyInt(), eq(true), eq(true), eq(true), eq(voteId), eq(true), eq(true), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
+8
-4
@@ -1,4 +1,8 @@
|
||||
package com.bencodez.votingplugin.backendproxy.messaging;
|
||||
package com.bencodez.votingplugin.tests.backendproxy.messaging;
|
||||
|
||||
import com.bencodez.votingplugin.tests.backendproxy.cache.ProcessedVoteCacheAccess;
|
||||
|
||||
import com.bencodez.votingplugin.backendproxy.messaging.BackendProxyMessageRouter;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -92,7 +96,7 @@ class BackendProxyVoteDelayReplayTest {
|
||||
fixture.assertEffects(1);
|
||||
fixture.assertOccurrence(0, voteId, RECORDED_TIME - 60_000L);
|
||||
verify(fixture.site, never()).giveWaitUntilVoteDelayRewards(any(), anyBoolean(), anyBoolean());
|
||||
assertTrue(new ProcessedVoteCache(fixture.receipts).hasDurableReceipt(voteId));
|
||||
assertTrue(ProcessedVoteCacheAccess.create(fixture.receipts).hasDurableReceipt(voteId));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -262,7 +266,7 @@ class BackendProxyVoteDelayReplayTest {
|
||||
pluginField.set(user, plugin);
|
||||
doCallRealMethod().when(user).bungeeVotePluginMessagingEvent(any(), anyLong(), any(VoteTotalsSnapshot.class),
|
||||
anyBoolean(), anyBoolean(), anyBoolean(), anyInt(), anyBoolean(), anyBoolean(),
|
||||
anyBoolean(), any(), anyBoolean());
|
||||
anyBoolean(), any(), anyBoolean(), anyBoolean(), anyBoolean());
|
||||
PlayerVoteListener listener = new PlayerVoteListener(plugin);
|
||||
var eventManager = plugin.getServer().getPluginManager();
|
||||
doAnswer(call -> {
|
||||
@@ -277,7 +281,7 @@ class BackendProxyVoteDelayReplayTest {
|
||||
private void restartReceiver() {
|
||||
router = new BackendProxyMessageRouter(plugin, mock(BackendPresenceManager.class),
|
||||
mock(BackendGlobalDataSync.class), mock(BackendVotePartySync.class),
|
||||
new ProcessedVoteCache(receipts));
|
||||
ProcessedVoteCacheAccess.create(receipts));
|
||||
}
|
||||
|
||||
private void deliver(JsonEnvelope envelope) {
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.bencodez.votingplugin.tests.backendproxy.transport;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.backendproxy.transport.*;
|
||||
import com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport.*;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope;
|
||||
import com.bencodez.simpleapi.servercomm.global.GlobalMessageHandler;
|
||||
import com.bencodez.simpleapi.servercomm.http.HttpBackendTransportConnector;
|
||||
import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode;
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
import com.bencodez.votingplugin.proxy.security.TransportEnvelopeEncryption;
|
||||
|
||||
/** Typed test-only access to package-private HttpBackendProxyTransport seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class HttpBackendProxyTransportAccess {
|
||||
public static void validate(HttpBackendProxyTransport target) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "validate", new String[]{}, new Object[]{}); }
|
||||
public static void validate(HttpBackendProxyTransport target, long deadlineNanos) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "validate", new String[]{"long"}, new Object[]{deadlineNanos}); }
|
||||
public static void activateRestoredHandoffDrain(HttpBackendProxyTransport target, int managerMessages) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "activateRestoredHandoffDrain", new String[]{"int"}, new Object[]{managerMessages}); }
|
||||
public static void appendShutdownHandoffMessages(HttpBackendProxyTransport target, java.util.List<JsonEnvelope> messages) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "appendShutdownHandoffMessages", new String[]{"List"}, new Object[]{messages}); }
|
||||
public static void acceptHandoffMessages(HttpBackendProxyTransport target, java.util.List<JsonEnvelope> messages) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "acceptHandoffMessages", new String[]{"List"}, new Object[]{messages}); }
|
||||
public static void beginPreparedHandoff(HttpBackendProxyTransport target) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "beginPreparedHandoff", new String[]{}, new Object[]{}); }
|
||||
public static void flushForReplacement(HttpBackendProxyTransport target, HttpBackendTransportConnector connector, long deadlineNanos) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "flushForReplacement", new String[]{"HttpBackendTransportConnector", "long"}, new Object[]{connector, deadlineNanos}); }
|
||||
public static java.util.List<JsonEnvelope> drainPreparedMessages(HttpBackendProxyTransport target) { return (java.util.List<JsonEnvelope>) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "drainPreparedMessages", new String[]{}, new Object[]{}); }
|
||||
public static boolean enrollForStartup(HttpBackendProxyTransport target, HttpConnectionCode code, String serverId, Path directory, boolean retryEnrollment, java.util.function.LongPredicate waitForRetry) throws Exception { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "enrollForStartup", new String[]{"HttpConnectionCode", "String", "Path", "boolean", "LongPredicate"}, new Object[]{code, serverId, directory, retryEnrollment, waitForRetry}); }
|
||||
public static void dispatchAfterPublication(HttpBackendProxyTransport target, GlobalMessageHandler messageHandler, JsonEnvelope envelope) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "dispatchAfterPublication", new String[]{"GlobalMessageHandler", "JsonEnvelope"}, new Object[]{messageHandler, envelope}); }
|
||||
private HttpBackendProxyTransportAccess() { }
|
||||
public static HttpBackendProxyTransport create(VotingPluginMain plugin) { return (HttpBackendProxyTransport) ReflectiveAccess.construct("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", new String[]{"VotingPluginMain"}, new Object[]{plugin}); }
|
||||
public static HttpBackendProxyTransport create(VotingPluginMain plugin, TransportEnvelopeEncryption encryption) { return (HttpBackendProxyTransport) ReflectiveAccess.construct("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", new String[]{"VotingPluginMain", "TransportEnvelopeEncryption"}, new Object[]{plugin, encryption}); }
|
||||
public static HttpBackendProxyTransport recreatePrepared(HttpBackendProxyTransport target) { return (HttpBackendProxyTransport) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "recreatePrepared", new String[]{}, new Object[]{}); }
|
||||
public static boolean isClosedForReplacement(HttpBackendProxyTransport target) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "isClosedForReplacement", new String[]{}, new Object[]{}); }
|
||||
|
||||
public static int preparedMessageCount(HttpBackendProxyTransport target) { return (Integer) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "preparedMessageCount", new String[]{}, new Object[]{}); }
|
||||
public static java.util.List<JsonEnvelope> preparedMessagesSnapshot(HttpBackendProxyTransport target) { return (java.util.List<JsonEnvelope>) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "preparedMessagesSnapshot", new String[]{}, new Object[]{}); }
|
||||
|
||||
public static void finishInitialization(boolean installed, HttpBackendTransportConnector replacement, boolean acquired, Semaphore owner, CountDownLatch completion) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", null, "finishInitialization", new String[]{"boolean", "HttpBackendTransportConnector", "boolean", "Semaphore", "CountDownLatch"}, new Object[]{installed, replacement, acquired, owner, completion}); }
|
||||
public static void transferStartupQueue(ArrayDeque<JsonEnvelope> queue, HttpBackendTransportConnector connector) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", null, "transferStartupQueue", new String[]{"ArrayDeque", "HttpBackendTransportConnector"}, new Object[]{queue, connector}); }
|
||||
|
||||
public static void restoreUnenrolledCredentialState(Path directory, String serverId, String configuredCode) throws Exception { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", null, "restoreUnenrolledCredentialState", new String[]{"Path", "String", "String"}, new Object[]{directory, serverId, configuredCode}); }
|
||||
|
||||
public static void dispatchIncoming(HttpBackendProxyTransport target, GlobalMessageHandler messageHandler, JsonEnvelope envelope, long deadlineNanos) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "dispatchIncoming", new String[]{"GlobalMessageHandler", "JsonEnvelope", "long"}, new Object[]{messageHandler, envelope, deadlineNanos}); }
|
||||
|
||||
|
||||
|
||||
|
||||
public static java.util.List<JsonEnvelope> handoffMessagesSnapshot(HttpBackendProxyTransport target) { return (java.util.List<JsonEnvelope>) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", target, "handoffMessagesSnapshot", new String[]{}, new Object[]{}); }
|
||||
|
||||
|
||||
public static HttpConnectionCode enrollmentCode(Path directory, String serverId, String configuredCode) { return (HttpConnectionCode) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", null, "enrollmentCode", new String[]{"Path", "String", "String"}, new Object[]{directory, serverId, configuredCode}); }
|
||||
public static boolean flushHandoffForShutdown(HttpBackendTransportConnector active, java.util.List<JsonEnvelope> finalHandoff, long deadlineNanos) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport", null, "flushHandoffForShutdown", new String[]{"HttpBackendTransportConnector", "List", "long"}, new Object[]{active, finalHandoff, deadlineNanos}); }
|
||||
}
|
||||
+93
-88
@@ -1,4 +1,10 @@
|
||||
package com.bencodez.votingplugin.backendproxy.transport;
|
||||
package com.bencodez.votingplugin.tests.backendproxy.transport;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import com.bencodez.votingplugin.backendproxy.transport.BackendProxyTransport;
|
||||
import com.bencodez.votingplugin.backendproxy.transport.BackendProxyTransportManager;
|
||||
import com.bencodez.votingplugin.backendproxy.transport.HttpBackendProxyTransport;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -64,7 +70,7 @@ class HttpBackendProxyTransportTest {
|
||||
}).when(scheduler).runTask(org.mockito.ArgumentMatchers.eq(plugin), org.mockito.ArgumentMatchers.any(Runnable.class));
|
||||
JsonEnvelope envelope = JsonEnvelope.builder("test").put("message", "payload").build();
|
||||
|
||||
new HttpBackendProxyTransport(plugin).dispatchIncoming(handler, envelope,
|
||||
HttpBackendProxyTransportAccess.dispatchIncoming(new HttpBackendProxyTransport(plugin), handler, envelope,
|
||||
System.nanoTime() + TimeUnit.SECONDS.toNanos(1));
|
||||
|
||||
assertTrue(callbackFinished.await(1, TimeUnit.SECONDS));
|
||||
@@ -73,7 +79,7 @@ class HttpBackendProxyTransportTest {
|
||||
|
||||
@Test
|
||||
void publicationActivatesTheSimpleApiInboundBarrier() throws Exception {
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
|
||||
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
|
||||
setField(transport, "connector", connector);
|
||||
|
||||
@@ -84,10 +90,10 @@ class HttpBackendProxyTransportTest {
|
||||
|
||||
@Test
|
||||
void closedPublicationGateRejectsDeliveryForReplay() {
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
|
||||
transport.close();
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> transport.dispatchAfterPublication(
|
||||
assertThrows(IllegalStateException.class, () -> HttpBackendProxyTransportAccess.dispatchAfterPublication(transport,
|
||||
mock(GlobalMessageHandler.class), JsonEnvelope.builder("test").build()));
|
||||
}
|
||||
|
||||
@@ -98,7 +104,7 @@ class HttpBackendProxyTransportTest {
|
||||
when(connector.send(pending)).thenReturn(false, true);
|
||||
when(connector.flushOutgoing(org.mockito.ArgumentMatchers.anyLong())).thenReturn(true);
|
||||
|
||||
assertTrue(HttpBackendProxyTransport.flushHandoffForShutdown(connector, List.of(pending),
|
||||
assertTrue(HttpBackendProxyTransportAccess.flushHandoffForShutdown(connector, List.of(pending),
|
||||
System.nanoTime() + TimeUnit.SECONDS.toNanos(1)));
|
||||
|
||||
org.mockito.InOrder order = org.mockito.Mockito.inOrder(connector);
|
||||
@@ -109,7 +115,7 @@ class HttpBackendProxyTransportTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void unpublishedHandoffMessagesAreDiscardedOnRollback() throws Exception {
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
|
||||
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
|
||||
setField(transport, "connector", connector);
|
||||
((java.util.ArrayDeque<JsonEnvelope>) field(transport, "handoffQueue"))
|
||||
@@ -130,7 +136,7 @@ class HttpBackendProxyTransportTest {
|
||||
when(connector.send(pending)).thenReturn(false);
|
||||
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> HttpBackendProxyTransport.transferStartupQueue(queue, connector));
|
||||
() -> HttpBackendProxyTransportAccess.transferStartupQueue(queue, connector));
|
||||
assertEquals(java.util.List.of(pending), new java.util.ArrayList<>(queue));
|
||||
}
|
||||
|
||||
@@ -142,7 +148,7 @@ class HttpBackendProxyTransportTest {
|
||||
CountDownLatch completion = new CountDownLatch(1);
|
||||
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> HttpBackendProxyTransport.finishInitialization(false, connector, true, owner, completion));
|
||||
() -> HttpBackendProxyTransportAccess.finishInitialization(false, connector, true, owner, completion));
|
||||
|
||||
verify(connector).close();
|
||||
assertEquals(1, owner.availablePermits());
|
||||
@@ -152,7 +158,7 @@ class HttpBackendProxyTransportTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void orderlyShutdownFlushesRecoveryQueueBeforeHandoffQueue() throws Exception {
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
|
||||
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
|
||||
JsonEnvelope recovery = JsonEnvelope.builder("recovery").build();
|
||||
JsonEnvelope handoff = JsonEnvelope.builder("handoff").build();
|
||||
@@ -181,8 +187,8 @@ class HttpBackendProxyTransportTest {
|
||||
setField(manager, "transport", failed);
|
||||
org.mockito.Mockito.doThrow(new IllegalStateException("worker still stopping"))
|
||||
.when(failed).prepareForReplacement();
|
||||
when(failed.isClosedForReplacement()).thenReturn(true);
|
||||
when(failed.recreatePrepared()).thenReturn(restored);
|
||||
when(HttpBackendProxyTransportAccess.isClosedForReplacement(failed)).thenReturn(true);
|
||||
when(HttpBackendProxyTransportAccess.recreatePrepared(failed)).thenReturn(restored);
|
||||
|
||||
assertThrows(IllegalStateException.class, manager::prepareForReplacement);
|
||||
manager.send(queued);
|
||||
@@ -203,7 +209,7 @@ class HttpBackendProxyTransportTest {
|
||||
manager.send(queued);
|
||||
|
||||
verify(active).send(queued);
|
||||
verify(active, org.mockito.Mockito.never()).recreatePrepared();
|
||||
HttpBackendProxyTransportAccess.recreatePrepared(verify(active, org.mockito.Mockito.never()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -267,7 +273,7 @@ class HttpBackendProxyTransportTest {
|
||||
});
|
||||
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> new HttpBackendProxyTransport(plugin).dispatchIncoming(handler,
|
||||
() -> HttpBackendProxyTransportAccess.dispatchIncoming(new HttpBackendProxyTransport(plugin), handler,
|
||||
JsonEnvelope.builder("test").build(), System.nanoTime()));
|
||||
assertTrue(scheduled.get() != null);
|
||||
scheduled.get().run();
|
||||
@@ -294,7 +300,7 @@ class HttpBackendProxyTransportTest {
|
||||
AtomicReference<Throwable> dispatchFailure = new AtomicReference<>();
|
||||
Thread dispatch = new Thread(() -> {
|
||||
try {
|
||||
new HttpBackendProxyTransport(plugin).dispatchIncoming(handler,
|
||||
HttpBackendProxyTransportAccess.dispatchIncoming(new HttpBackendProxyTransport(plugin), handler,
|
||||
JsonEnvelope.builder("test").build(), System.nanoTime() + TimeUnit.SECONDS.toNanos(5));
|
||||
} catch (Throwable thrown) {
|
||||
dispatchFailure.set(thrown);
|
||||
@@ -317,28 +323,28 @@ class HttpBackendProxyTransportTest {
|
||||
when(plugin.getDataFolder()).thenReturn(directory.toFile());
|
||||
when(plugin.getBungeeSettings()).thenReturn(settings);
|
||||
when(settings.getServer()).thenReturn("lobby-1");
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
|
||||
when(settings.getHttpConnectionCode()).thenReturn("malformed");
|
||||
assertThrows(IllegalStateException.class, transport::validate);
|
||||
assertThrows(IllegalStateException.class, () -> HttpBackendProxyTransportAccess.validate(transport));
|
||||
|
||||
when(settings.getHttpConnectionCode()).thenReturn(code("lobby-1", Instant.now().minusSeconds(1)).encode());
|
||||
assertThrows(IllegalStateException.class, transport::validate);
|
||||
assertThrows(IllegalStateException.class, () -> HttpBackendProxyTransportAccess.validate(transport));
|
||||
|
||||
when(settings.getHttpConnectionCode()).thenReturn(code("survival", Instant.now().plusSeconds(60)).encode());
|
||||
assertThrows(IllegalStateException.class, transport::validate);
|
||||
assertThrows(IllegalStateException.class, () -> HttpBackendProxyTransportAccess.validate(transport));
|
||||
assertThrows(IllegalStateException.class, () -> transport.start(mock(GlobalMessageHandler.class)),
|
||||
"invalid configuration must fail before the enrollment worker starts");
|
||||
|
||||
when(settings.getHttpConnectionCode()).thenReturn(code("lobby-1", Instant.now().plusSeconds(60)).encode());
|
||||
assertDoesNotThrow(() -> transport.validate());
|
||||
assertDoesNotThrow(() -> HttpBackendProxyTransportAccess.validate(transport));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ordinaryStartupRetriesTransientInitialEnrollmentWithBoundedBackoff() throws Exception {
|
||||
VotingPluginMain plugin = mock(VotingPluginMain.class);
|
||||
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
HttpConnectionCode enrollment = code("lobby-1", Instant.now().plusSeconds(60));
|
||||
List<Long> delays = new ArrayList<>();
|
||||
java.util.concurrent.atomic.AtomicInteger attempts = new java.util.concurrent.atomic.AtomicInteger();
|
||||
@@ -349,7 +355,7 @@ class HttpBackendProxyTransportTest {
|
||||
if (attempts.getAndIncrement() == 0) throw new IOException("proxy unavailable");
|
||||
return null;
|
||||
});
|
||||
assertTrue(transport.enrollForStartup(enrollment, "lobby-1", directory, true,
|
||||
assertTrue(HttpBackendProxyTransportAccess.enrollForStartup(transport, enrollment, "lobby-1", directory, true,
|
||||
delay -> { delays.add(delay); return true; }));
|
||||
}
|
||||
|
||||
@@ -359,11 +365,11 @@ class HttpBackendProxyTransportTest {
|
||||
|
||||
@Test
|
||||
void replacementCancelsRetryingInitialEnrollmentWithoutCredential() throws Exception {
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
|
||||
Path credentials = directory.resolve("http");
|
||||
java.lang.reflect.Field directoryField = HttpBackendProxyTransport.class.getDeclaredField("configuredDirectory");
|
||||
directoryField.setAccessible(true);
|
||||
directoryField.set(transport, credentials);
|
||||
ReflectiveAccess.set(directoryField, transport, credentials);
|
||||
java.lang.reflect.Field ownersField = HttpBackendProxyTransport.class.getDeclaredField("DIRECTORY_OWNERS");
|
||||
ownersField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -380,7 +386,7 @@ class HttpBackendProxyTransportTest {
|
||||
});
|
||||
java.lang.reflect.Field workerField = HttpBackendProxyTransport.class.getDeclaredField("worker");
|
||||
workerField.setAccessible(true);
|
||||
workerField.set(transport, retryingEnrollment);
|
||||
ReflectiveAccess.set(workerField, transport, retryingEnrollment);
|
||||
retryingEnrollment.start();
|
||||
assertTrue(workerStarted.await(1, TimeUnit.SECONDS));
|
||||
JsonEnvelope queued = JsonEnvelope.builder("queued-before-replacement").build();
|
||||
@@ -392,7 +398,7 @@ class HttpBackendProxyTransportTest {
|
||||
HttpBackendProxyTransport.class.getDeclaredField("restoreUnenrolledState");
|
||||
restoreAbsent.setAccessible(true);
|
||||
assertTrue(restoreAbsent.getBoolean(transport), "rollback must remember that no credential existed");
|
||||
assertEquals(List.of(queued), transport.drainPreparedMessages(),
|
||||
assertEquals(List.of(queued), HttpBackendProxyTransportAccess.drainPreparedMessages(transport),
|
||||
"messages accepted before replacement must remain available for handoff");
|
||||
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
|
||||
@@ -404,7 +410,7 @@ class HttpBackendProxyTransportTest {
|
||||
|
||||
@Test
|
||||
void replacementSnapshotsInitialEnrollmentPublishedWhileCancellationCompletes() throws Exception {
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
|
||||
Path credentials = directory.resolve("http");
|
||||
HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("original-proxy"),
|
||||
"proxy.example.test");
|
||||
@@ -451,7 +457,7 @@ class HttpBackendProxyTransportTest {
|
||||
assertTrue(HttpClientCredentialStore.hasEnrolledProfile(credentials));
|
||||
|
||||
HttpConnectionCode original = code("lobby-1", Instant.now().plusSeconds(60));
|
||||
HttpBackendProxyTransport.restoreUnenrolledCredentialState(credentials, "lobby-1", original.encode());
|
||||
HttpBackendProxyTransportAccess.restoreUnenrolledCredentialState(credentials, "lobby-1", original.encode());
|
||||
|
||||
assertFalse(HttpClientCredentialStore.hasEnrolledProfile(credentials),
|
||||
"a failed staged first enrollment must not remain active during rollback");
|
||||
@@ -466,7 +472,7 @@ class HttpBackendProxyTransportTest {
|
||||
identity.serverCertificatePin(), identity.caCertificatePin(), Instant.now().plusSeconds(60), "F".repeat(43));
|
||||
HttpClientCredentialStore.saveEnrolled(credentials, original, identity.issueClientCertificate("lobby-1"));
|
||||
|
||||
HttpBackendProxyTransport.restoreUnenrolledCredentialState(credentials, "lobby-1", original.encode());
|
||||
HttpBackendProxyTransportAccess.restoreUnenrolledCredentialState(credentials, "lobby-1", original.encode());
|
||||
|
||||
assertTrue(HttpClientCredentialStore.hasEnrolledProfile(credentials),
|
||||
"a credential issued for the consumed original code must remain recoverable");
|
||||
@@ -476,13 +482,13 @@ class HttpBackendProxyTransportTest {
|
||||
void stagedEnrollmentRemainsFailFast() throws Exception {
|
||||
VotingPluginMain plugin = mock(VotingPluginMain.class);
|
||||
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
HttpConnectionCode enrollment = code("lobby-1", Instant.now().plusSeconds(60));
|
||||
try (MockedStatic<HttpBackendTransportConnector> connector = org.mockito.Mockito.mockStatic(HttpBackendTransportConnector.class)) {
|
||||
connector.when(() -> HttpBackendTransportConnector.enroll(enrollment, "lobby-1", directory))
|
||||
.thenThrow(new IOException("proxy unavailable"));
|
||||
assertThrows(IOException.class,
|
||||
() -> transport.enrollForStartup(enrollment, "lobby-1", directory, false,
|
||||
() -> HttpBackendProxyTransportAccess.enrollForStartup(transport, enrollment, "lobby-1", directory, false,
|
||||
delay -> { throw new AssertionError("staged enrollment must not retry"); }));
|
||||
}
|
||||
}
|
||||
@@ -494,32 +500,32 @@ class HttpBackendProxyTransportTest {
|
||||
HttpConnectionCode original = new HttpConnectionCode("lobby-1", URI.create("https://proxy.example.test:1297/"),
|
||||
identity.serverCertificatePin(), identity.caCertificatePin(), Instant.now().minusSeconds(1), "A".repeat(43));
|
||||
HttpClientCredentialStore.saveEnrolled(credentials, original, identity.issueClientCertificate("lobby-1"));
|
||||
assertNull(HttpBackendProxyTransport.enrollmentCode(credentials, "lobby-1", ""));
|
||||
assertNull(HttpBackendProxyTransport.enrollmentCode(credentials, "lobby-1", original.encode()),
|
||||
assertNull(HttpBackendProxyTransportAccess.enrollmentCode(credentials, "lobby-1", ""));
|
||||
assertNull(HttpBackendProxyTransportAccess.enrollmentCode(credentials, "lobby-1", original.encode()),
|
||||
"the already-consumed code must not be retried, even after it expires");
|
||||
|
||||
HttpConnectionCode replacement = new HttpConnectionCode("lobby-1", original.endpoint(), original.serverCertificatePin(),
|
||||
original.caCertificatePin(), Instant.now().plusSeconds(60), "B".repeat(43));
|
||||
assertEquals(replacement.encode(), HttpBackendProxyTransport.enrollmentCode(credentials, "lobby-1", replacement.encode()).encode());
|
||||
assertEquals(replacement.encode(), HttpBackendProxyTransportAccess.enrollmentCode(credentials, "lobby-1", replacement.encode()).encode());
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> HttpBackendProxyTransport.enrollmentCode(credentials, "lobby-1", "malformed"));
|
||||
() -> HttpBackendProxyTransportAccess.enrollmentCode(credentials, "lobby-1", "malformed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedReplacementFlushResumesTheExistingConnector() {
|
||||
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
|
||||
when(connector.flushOutgoing(org.mockito.ArgumentMatchers.anyLong())).thenReturn(false);
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
|
||||
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> transport.flushForReplacement(connector, System.nanoTime()));
|
||||
() -> HttpBackendProxyTransportAccess.flushForReplacement(transport, connector, System.nanoTime()));
|
||||
|
||||
verify(connector).start();
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeNeverWaitsForSetupOnTheCallingThread() throws Exception {
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
|
||||
CountDownLatch started = new CountDownLatch(1), release = new CountDownLatch(1);
|
||||
Thread blocked = new Thread(() -> {
|
||||
started.countDown();
|
||||
@@ -530,7 +536,7 @@ class HttpBackendProxyTransportTest {
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
java.lang.reflect.Field worker = HttpBackendProxyTransport.class.getDeclaredField("worker");
|
||||
worker.setAccessible(true);
|
||||
worker.set(transport, blocked);
|
||||
ReflectiveAccess.set(worker, transport, blocked);
|
||||
|
||||
long startedAt = System.nanoTime();
|
||||
transport.close();
|
||||
@@ -541,7 +547,7 @@ class HttpBackendProxyTransportTest {
|
||||
|
||||
@Test
|
||||
void prepareForReplacementSnapshotsThePersistedCredentialGeneration() throws Exception {
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
|
||||
Path credentials = directory.resolve("http");
|
||||
HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("replacement-proxy"), "proxy.example.test");
|
||||
HttpConnectionCode code = new HttpConnectionCode("lobby-1", URI.create("https://proxy.example.test:1297/"),
|
||||
@@ -551,12 +557,12 @@ class HttpBackendProxyTransportTest {
|
||||
HttpClientCredentialStore.snapshotActiveGeneration(credentials);
|
||||
java.lang.reflect.Field directoryField = HttpBackendProxyTransport.class.getDeclaredField("configuredDirectory");
|
||||
directoryField.setAccessible(true);
|
||||
directoryField.set(transport, credentials);
|
||||
ReflectiveAccess.set(directoryField, transport, credentials);
|
||||
java.lang.reflect.Field connectorField = HttpBackendProxyTransport.class.getDeclaredField("connector");
|
||||
connectorField.setAccessible(true);
|
||||
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
|
||||
when(connector.flushOutgoing(org.mockito.ArgumentMatchers.anyLong())).thenReturn(true);
|
||||
connectorField.set(transport, connector);
|
||||
ReflectiveAccess.set(connectorField, transport, connector);
|
||||
|
||||
assertDoesNotThrow(transport::prepareForReplacement);
|
||||
java.lang.reflect.Field generationField =
|
||||
@@ -585,12 +591,12 @@ class HttpBackendProxyTransportTest {
|
||||
var owners = (java.util.concurrent.ConcurrentHashMap<Path, java.util.concurrent.Semaphore>) ownersField.get(null);
|
||||
java.util.concurrent.Semaphore predecessor = new java.util.concurrent.Semaphore(0);
|
||||
owners.put(credentials.toAbsolutePath().normalize(), predecessor);
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
transport.start(mock(GlobalMessageHandler.class));
|
||||
java.util.concurrent.atomic.AtomicReference<Throwable> failure = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
CountDownLatch finished = new CountDownLatch(1);
|
||||
Thread validation = new Thread(() -> {
|
||||
try { transport.validate(System.nanoTime() + TimeUnit.SECONDS.toNanos(1)); }
|
||||
try { HttpBackendProxyTransportAccess.validate(transport, System.nanoTime() + TimeUnit.SECONDS.toNanos(1)); }
|
||||
catch (Throwable thrown) { failure.set(thrown); }
|
||||
finally { finished.countDown(); }
|
||||
});
|
||||
@@ -611,32 +617,32 @@ class HttpBackendProxyTransportTest {
|
||||
void preparedHandoffMessagesStayAheadOfNewlyPublishedSendsWhenConnectorIsFull() throws Exception {
|
||||
VotingPluginMain plugin = mock(VotingPluginMain.class);
|
||||
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
|
||||
when(connector.send(org.mockito.ArgumentMatchers.any(JsonEnvelope.class))).thenReturn(false);
|
||||
setField(transport, "connector", connector);
|
||||
JsonEnvelope acceptedBeforePublication = JsonEnvelope.builder("old").build();
|
||||
JsonEnvelope sentAfterPublication = JsonEnvelope.builder("new").build();
|
||||
|
||||
transport.beginPreparedHandoff();
|
||||
HttpBackendProxyTransportAccess.beginPreparedHandoff(transport);
|
||||
transport.send(sentAfterPublication);
|
||||
transport.acceptHandoffMessages(List.of(acceptedBeforePublication));
|
||||
HttpBackendProxyTransportAccess.acceptHandoffMessages(transport, List.of(acceptedBeforePublication));
|
||||
|
||||
assertEquals(List.of(acceptedBeforePublication, sentAfterPublication), transport.handoffMessagesSnapshot());
|
||||
assertEquals(List.of(acceptedBeforePublication, sentAfterPublication), HttpBackendProxyTransportAccess.handoffMessagesSnapshot(transport));
|
||||
transport.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shutdownManagerRemainderStaysBehindAlreadyTransferredPrefix() throws Exception {
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
|
||||
JsonEnvelope olderPrefix = JsonEnvelope.builder("older-prefix").build();
|
||||
JsonEnvelope newerRemainder = JsonEnvelope.builder("newer-remainder").build();
|
||||
((java.util.ArrayDeque<JsonEnvelope>) field(transport, "handoffQueue")).addLast(olderPrefix);
|
||||
|
||||
transport.appendShutdownHandoffMessages(List.of(newerRemainder));
|
||||
HttpBackendProxyTransportAccess.appendShutdownHandoffMessages(transport, List.of(newerRemainder));
|
||||
|
||||
assertEquals(List.of(olderPrefix, newerRemainder), transport.handoffMessagesSnapshot());
|
||||
assertEquals(List.of(olderPrefix, newerRemainder), HttpBackendProxyTransportAccess.handoffMessagesSnapshot(transport));
|
||||
transport.close();
|
||||
}
|
||||
|
||||
@@ -647,8 +653,8 @@ class HttpBackendProxyTransportTest {
|
||||
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
|
||||
BackendProxyTransportManager previous = new BackendProxyTransportManager(plugin);
|
||||
BackendProxyTransportManager replacement = new BackendProxyTransportManager(plugin);
|
||||
HttpBackendProxyTransport oldTransport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport newTransport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport oldTransport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
HttpBackendProxyTransport newTransport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
setField(previous, "preparedTransport", oldTransport);
|
||||
setField(replacement, "transport", newTransport);
|
||||
java.util.ArrayDeque<JsonEnvelope> oldQueue =
|
||||
@@ -677,12 +683,12 @@ class HttpBackendProxyTransportTest {
|
||||
|
||||
assertDoesNotThrow(() -> previous.completePreparedTransportHandoff(replacement));
|
||||
replacement.awaitAsyncHandoff(System.nanoTime() + TimeUnit.SECONDS.toNanos(2));
|
||||
List<JsonEnvelope> queued = newTransport.handoffMessagesSnapshot();
|
||||
List<JsonEnvelope> queued = HttpBackendProxyTransportAccess.handoffMessagesSnapshot(newTransport);
|
||||
assertEquals(4096, queued.size());
|
||||
assertEquals(oldMessages, queued.subList(0, 2048));
|
||||
assertEquals(preparedMessages, queued.subList(2048, 3072));
|
||||
assertEquals(replacementMessages.subList(0, 1024), queued.subList(3072, 4096));
|
||||
assertEquals(0, oldTransport.preparedMessageCount());
|
||||
assertEquals(0, HttpBackendProxyTransportAccess.preparedMessageCount(oldTransport));
|
||||
assertTrue(((java.util.ArrayDeque<JsonEnvelope>) field(previous, "preparedSends")).isEmpty());
|
||||
newTransport.close();
|
||||
}
|
||||
@@ -693,8 +699,8 @@ class HttpBackendProxyTransportTest {
|
||||
VotingPluginMain plugin = mock(VotingPluginMain.class);
|
||||
BackendProxyTransportManager previous = new BackendProxyTransportManager(plugin);
|
||||
BackendProxyTransportManager replacement = new BackendProxyTransportManager(plugin);
|
||||
HttpBackendProxyTransport oldTransport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport newTransport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport oldTransport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
HttpBackendProxyTransport newTransport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
setField(previous, "preparedTransport", oldTransport);
|
||||
setField(replacement, "transport", newTransport);
|
||||
java.util.ArrayDeque<JsonEnvelope> oldQueue =
|
||||
@@ -718,8 +724,8 @@ class HttpBackendProxyTransportTest {
|
||||
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
|
||||
BackendProxyTransportManager previous = new BackendProxyTransportManager(plugin);
|
||||
BackendProxyTransportManager replacement = new BackendProxyTransportManager(plugin);
|
||||
HttpBackendProxyTransport oldTransport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport newTransport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport oldTransport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
HttpBackendProxyTransport newTransport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
setField(previous, "preparedTransport", oldTransport);
|
||||
setField(replacement, "transport", newTransport);
|
||||
java.util.ArrayDeque<JsonEnvelope> oldQueue =
|
||||
@@ -739,7 +745,7 @@ class HttpBackendProxyTransportTest {
|
||||
assertEquals(3072, oldQueue.size());
|
||||
assertEquals(1024, ((java.util.ArrayDeque<?>) field(previous, "preparedSends")).size());
|
||||
assertEquals(2, ((java.util.ArrayDeque<?>) field(replacement, "preparedSends")).size());
|
||||
assertTrue(newTransport.handoffMessagesSnapshot().isEmpty());
|
||||
assertTrue(HttpBackendProxyTransportAccess.handoffMessagesSnapshot(newTransport).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -748,7 +754,7 @@ class HttpBackendProxyTransportTest {
|
||||
VotingPluginMain plugin = mock(VotingPluginMain.class);
|
||||
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
|
||||
BackendProxyTransportManager manager = new BackendProxyTransportManager(plugin);
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
setField(manager, "transport", transport);
|
||||
setField(transport, "managerHandoffActive", true);
|
||||
java.util.ArrayDeque<JsonEnvelope> httpQueue =
|
||||
@@ -772,7 +778,7 @@ class HttpBackendProxyTransportTest {
|
||||
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
|
||||
BackendProxyTransportManager previous = new BackendProxyTransportManager(plugin);
|
||||
BackendProxyTransportManager replacement = new BackendProxyTransportManager(plugin);
|
||||
HttpBackendProxyTransport oldTransport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport oldTransport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
HttpBackendProxyTransport newTransport = mock(HttpBackendProxyTransport.class);
|
||||
JsonEnvelope envelope = JsonEnvelope.builder("accepted-once").build();
|
||||
CountDownLatch sending = new CountDownLatch(1);
|
||||
@@ -801,8 +807,7 @@ class HttpBackendProxyTransportTest {
|
||||
closing.join(TimeUnit.SECONDS.toMillis(2));
|
||||
assertFalse(closing.isAlive());
|
||||
verify(newTransport).send(envelope);
|
||||
verify(newTransport, org.mockito.Mockito.never())
|
||||
.acceptHandoffMessages(org.mockito.ArgumentMatchers.anyList());
|
||||
HttpBackendProxyTransportAccess.acceptHandoffMessages(verify(newTransport, org.mockito.Mockito.never()), org.mockito.ArgumentMatchers.anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -812,8 +817,8 @@ class HttpBackendProxyTransportTest {
|
||||
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
|
||||
BackendProxyTransportManager previous = new BackendProxyTransportManager(plugin);
|
||||
BackendProxyTransportManager replacement = new BackendProxyTransportManager(plugin);
|
||||
HttpBackendProxyTransport oldTransport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport newTransport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport oldTransport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
HttpBackendProxyTransport newTransport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
|
||||
JsonEnvelope source = JsonEnvelope.builder("source").build();
|
||||
JsonEnvelope adapted = JsonEnvelope.builder("adapted").build();
|
||||
@@ -874,7 +879,7 @@ class HttpBackendProxyTransportTest {
|
||||
VotingPluginMain plugin = mock(VotingPluginMain.class);
|
||||
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
|
||||
BackendProxyTransportManager manager = new BackendProxyTransportManager(plugin);
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
|
||||
JsonEnvelope envelope = JsonEnvelope.builder("shutdown").build();
|
||||
CountDownLatch sending = new CountDownLatch(1);
|
||||
@@ -941,8 +946,8 @@ class HttpBackendProxyTransportTest {
|
||||
VotingPluginMain plugin = mock(VotingPluginMain.class);
|
||||
BackendProxyTransportManager previous = new BackendProxyTransportManager(plugin);
|
||||
BackendProxyTransportManager replacement = new BackendProxyTransportManager(plugin);
|
||||
HttpBackendProxyTransport oldTransport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport newTransport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport oldTransport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
HttpBackendProxyTransport newTransport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
JsonEnvelope old = JsonEnvelope.builder("old").build();
|
||||
JsonEnvelope prepared = JsonEnvelope.builder("prepared").build();
|
||||
setField(previous, "preparedTransport", oldTransport);
|
||||
@@ -956,7 +961,7 @@ class HttpBackendProxyTransportTest {
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> previous.completePreparedTransportHandoff(replacement));
|
||||
|
||||
assertEquals(List.of(old), oldTransport.preparedMessagesSnapshot());
|
||||
assertEquals(List.of(old), HttpBackendProxyTransportAccess.preparedMessagesSnapshot(oldTransport));
|
||||
java.util.ArrayDeque<?> retained = (java.util.ArrayDeque<?>) field(previous, "preparedSends");
|
||||
assertEquals(1, retained.size());
|
||||
assertSame(prepared, field(retained.peekFirst(), "source"));
|
||||
@@ -966,13 +971,13 @@ class HttpBackendProxyTransportTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void preparedDrainPreservesStartupAndExistingHandoffMessagesInFifoOrder() throws Exception {
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
|
||||
JsonEnvelope startup = JsonEnvelope.builder("startup").build();
|
||||
JsonEnvelope handoff = JsonEnvelope.builder("handoff").build();
|
||||
((java.util.ArrayDeque<JsonEnvelope>) field(transport, "startupQueue")).add(startup);
|
||||
((java.util.ArrayDeque<JsonEnvelope>) field(transport, "handoffQueue")).add(handoff);
|
||||
|
||||
assertEquals(List.of(startup, handoff), transport.drainPreparedMessages());
|
||||
assertEquals(List.of(startup, handoff), HttpBackendProxyTransportAccess.drainPreparedMessages(transport));
|
||||
assertTrue(((java.util.ArrayDeque<JsonEnvelope>) field(transport, "startupQueue")).isEmpty());
|
||||
assertTrue(((java.util.ArrayDeque<JsonEnvelope>) field(transport, "handoffQueue")).isEmpty());
|
||||
}
|
||||
@@ -982,7 +987,7 @@ class HttpBackendProxyTransportTest {
|
||||
void rollbackRecreateKeepsAnOversizedCombinedQueueInTheDrainedHandoffLane() throws Exception {
|
||||
VotingPluginMain plugin = mock(VotingPluginMain.class);
|
||||
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
|
||||
Path credentials = directory.resolve("http");
|
||||
setField(transport, "configuredDirectory", credentials);
|
||||
setField(transport, "configuredServerId", "lobby-1");
|
||||
@@ -1009,7 +1014,7 @@ class HttpBackendProxyTransportTest {
|
||||
HttpBackendProxyTransport restored = null;
|
||||
Thread setup = null;
|
||||
try {
|
||||
restored = transport.recreatePrepared();
|
||||
restored = HttpBackendProxyTransportAccess.recreatePrepared(transport);
|
||||
assertTrue(((java.util.ArrayDeque<JsonEnvelope>) field(restored, "startupQueue")).isEmpty(),
|
||||
"rollback must not synchronously overfill the connector's 1024-entry queue");
|
||||
assertEquals(queued,
|
||||
@@ -1031,7 +1036,7 @@ class HttpBackendProxyTransportTest {
|
||||
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
|
||||
BackendProxyTransportManager manager = new BackendProxyTransportManager(plugin);
|
||||
HttpBackendProxyTransport previous = mock(HttpBackendProxyTransport.class);
|
||||
HttpBackendProxyTransport restored = new HttpBackendProxyTransport(plugin);
|
||||
HttpBackendProxyTransport restored = HttpBackendProxyTransportAccess.create(plugin);
|
||||
java.util.ArrayDeque<JsonEnvelope> restoredQueue =
|
||||
(java.util.ArrayDeque<JsonEnvelope>) field(restored, "handoffQueue");
|
||||
for (int index = 0; index < 4096; index++)
|
||||
@@ -1039,29 +1044,29 @@ class HttpBackendProxyTransportTest {
|
||||
JsonEnvelope first = JsonEnvelope.builder("fenced-1").build();
|
||||
JsonEnvelope second = JsonEnvelope.builder("fenced-2").build();
|
||||
setField(manager, "preparedTransport", previous);
|
||||
when(previous.recreatePrepared()).thenReturn(restored);
|
||||
when(HttpBackendProxyTransportAccess.recreatePrepared(previous)).thenReturn(restored);
|
||||
manager.send(first);
|
||||
manager.send(second);
|
||||
|
||||
manager.restorePreparedTransport();
|
||||
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
|
||||
while (restored.handoffMessagesSnapshot().size() != 4098 && System.nanoTime() < deadline)
|
||||
while (HttpBackendProxyTransportAccess.handoffMessagesSnapshot(restored).size() != 4098 && System.nanoTime() < deadline)
|
||||
Thread.onSpinWait();
|
||||
assertEquals(4098, restored.handoffMessagesSnapshot().size());
|
||||
assertSame(first, restored.handoffMessagesSnapshot().get(4096));
|
||||
assertSame(second, restored.handoffMessagesSnapshot().get(4097));
|
||||
assertEquals(4098, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(restored).size());
|
||||
assertSame(first, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(restored).get(4096));
|
||||
assertSame(second, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(restored).get(4097));
|
||||
assertTrue(((java.util.ArrayDeque<?>) field(manager, "asyncHandoffSends")).isEmpty());
|
||||
|
||||
manager.send(JsonEnvelope.builder("newer-rejected").build());
|
||||
assertEquals(4098, restored.handoffMessagesSnapshot().size(),
|
||||
assertEquals(4098, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(restored).size(),
|
||||
"normal sends must not expand the one-time rollback recovery allowance");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shutdownCanAppendTheReservedRollbackRemainder() throws Exception {
|
||||
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
|
||||
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
|
||||
java.util.ArrayDeque<JsonEnvelope> handoff =
|
||||
(java.util.ArrayDeque<JsonEnvelope>) field(transport, "handoffQueue");
|
||||
for (int index = 0; index < 4096; index++)
|
||||
@@ -1069,12 +1074,12 @@ class HttpBackendProxyTransportTest {
|
||||
JsonEnvelope first = JsonEnvelope.builder("fenced-1").build();
|
||||
JsonEnvelope second = JsonEnvelope.builder("fenced-2").build();
|
||||
|
||||
transport.activateRestoredHandoffDrain(2);
|
||||
transport.appendShutdownHandoffMessages(List.of(first, second));
|
||||
HttpBackendProxyTransportAccess.activateRestoredHandoffDrain(transport, 2);
|
||||
HttpBackendProxyTransportAccess.appendShutdownHandoffMessages(transport, List.of(first, second));
|
||||
|
||||
assertEquals(4098, transport.handoffMessagesSnapshot().size());
|
||||
assertSame(first, transport.handoffMessagesSnapshot().get(4096));
|
||||
assertSame(second, transport.handoffMessagesSnapshot().get(4097));
|
||||
assertEquals(4098, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(transport).size());
|
||||
assertSame(first, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(transport).get(4096));
|
||||
assertSame(second, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(transport).get(4097));
|
||||
}
|
||||
|
||||
private static HttpConnectionCode code(String serverId, Instant expiry) {
|
||||
@@ -1085,7 +1090,7 @@ class HttpBackendProxyTransportTest {
|
||||
private static void setField(Object target, String name, Object value) throws Exception {
|
||||
java.lang.reflect.Field field = target.getClass().getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
ReflectiveAccess.set(field, target, value);
|
||||
}
|
||||
|
||||
private static Object field(Object target, String name) throws Exception {
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.bencodez.votingplugin.tests.backendproxy.transport;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.backendproxy.transport.*;
|
||||
import com.bencodez.votingplugin.backendproxy.transport.MqttBackendProxyTransport.*;
|
||||
import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope;
|
||||
import com.bencodez.votingplugin.proxy.security.SharedTransportEnvelopeAuthenticator;
|
||||
import com.bencodez.votingplugin.proxy.security.TransportEnvelopeEncryption;
|
||||
|
||||
/** Typed test-only access to package-private MqttBackendProxyTransport seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class MqttBackendProxyTransportAccess {
|
||||
private MqttBackendProxyTransportAccess() { }
|
||||
public static void updateSecurity(MqttBackendProxyTransport target, SharedTransportEnvelopeAuthenticator replacementAuthenticator, TransportEnvelopeEncryption replacementEncryption) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.MqttBackendProxyTransport", target, "updateSecurity", new String[]{"SharedTransportEnvelopeAuthenticator", "TransportEnvelopeEncryption"}, new Object[]{replacementAuthenticator, replacementEncryption}); }
|
||||
public static void acceptAuthenticatedEnvelope(MqttBackendProxyTransport target, JsonEnvelope envelope, String topic) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.MqttBackendProxyTransport", target, "acceptAuthenticatedEnvelope", new String[]{"JsonEnvelope", "String"}, new Object[]{envelope, topic}); }
|
||||
}
|
||||
+12
-8
@@ -1,4 +1,8 @@
|
||||
package com.bencodez.votingplugin.backendproxy.transport;
|
||||
package com.bencodez.votingplugin.tests.backendproxy.transport;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import com.bencodez.votingplugin.backendproxy.transport.MqttBackendProxyTransport;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -34,12 +38,12 @@ class MqttBackendProxyTransportTest {
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
setField(transport, "messageHandler", messages);
|
||||
setField(transport, "authenticator", authenticator);
|
||||
transport.updateSecurity(authenticator, encryption);
|
||||
MqttBackendProxyTransportAccess.updateSecurity(transport, authenticator, encryption);
|
||||
JsonEnvelope vote = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).put("player", "Alex").build();
|
||||
JsonEnvelope signed = authenticator.sign(encryption.encrypt(vote), Domain.MQTT_PROXY_BACKEND, "proxy-a",
|
||||
"votingplugin/servers/backend-a");
|
||||
|
||||
transport.acceptAuthenticatedEnvelope(signed, "votingplugin/servers/backend-a");
|
||||
MqttBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "votingplugin/servers/backend-a");
|
||||
|
||||
verify(messages).onMessage(org.mockito.ArgumentMatchers.argThat(received ->
|
||||
VotingPluginWire.SUB_VOTE.equals(received.getSubChannel())
|
||||
@@ -55,9 +59,9 @@ class MqttBackendProxyTransportTest {
|
||||
setField(transport, "authenticator", authenticator);
|
||||
JsonEnvelope vote = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).put("player", "Alex").build();
|
||||
|
||||
transport.acceptAuthenticatedEnvelope(authenticator.sign(vote, Domain.MQTT_PROXY_BACKEND, "proxy-a", "votingplugin/servers/backend-a"),
|
||||
MqttBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, authenticator.sign(vote, Domain.MQTT_PROXY_BACKEND, "proxy-a", "votingplugin/servers/backend-a"),
|
||||
"votingplugin/servers/backend-a");
|
||||
transport.acceptAuthenticatedEnvelope(vote, "votingplugin/servers/backend-a");
|
||||
MqttBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, vote, "votingplugin/servers/backend-a");
|
||||
|
||||
verify(messages).onMessage(org.mockito.ArgumentMatchers.argThat(received ->
|
||||
VotingPluginWire.SUB_VOTE.equals(received.getSubChannel())
|
||||
@@ -76,9 +80,9 @@ class MqttBackendProxyTransportTest {
|
||||
JsonEnvelope signed = authenticator.sign(vote, Domain.MQTT_PROXY_BACKEND, "proxy-a",
|
||||
"votingplugin/servers/backend-a");
|
||||
|
||||
transport.acceptAuthenticatedEnvelope(signed, "votingplugin/servers/backend-b");
|
||||
MqttBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "votingplugin/servers/backend-b");
|
||||
org.mockito.Mockito.verifyNoInteractions(messages);
|
||||
transport.acceptAuthenticatedEnvelope(signed, "votingplugin/servers/backend-a");
|
||||
MqttBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "votingplugin/servers/backend-a");
|
||||
verify(messages).onMessage(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@@ -92,6 +96,6 @@ class MqttBackendProxyTransportTest {
|
||||
private static void setField(Object target, String name, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
ReflectiveAccess.set(field, target, value);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.bencodez.votingplugin.tests.backendproxy.transport;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.backendproxy.transport.*;
|
||||
import com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport.*;
|
||||
import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope;
|
||||
import com.bencodez.votingplugin.proxy.security.SharedTransportEnvelopeAuthenticator;
|
||||
import com.bencodez.votingplugin.proxy.security.TransportEnvelopeEncryption;
|
||||
import redis.clients.jedis.DefaultJedisClientConfig;
|
||||
|
||||
/** Typed test-only access to package-private RedisBackendProxyTransport seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class RedisBackendProxyTransportAccess {
|
||||
public static final int MAX_LEGACY_HANDOFF_DELIVERIES = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport", "MAX_LEGACY_HANDOFF_DELIVERIES");
|
||||
public static final int MAX_IDENTIFIED_HANDOFF_DELIVERIES = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport", "MAX_IDENTIFIED_HANDOFF_DELIVERIES");
|
||||
public static final int MAX_REPLAY_HANDOFF_DELIVERIES = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport", "MAX_REPLAY_HANDOFF_DELIVERIES");
|
||||
public static final int REPLAY_BATCH_SIZE = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport", "REPLAY_BATCH_SIZE");
|
||||
private RedisBackendProxyTransportAccess() { }
|
||||
public static void updateSecurity(RedisBackendProxyTransport target, SharedTransportEnvelopeAuthenticator replacementAuthenticator, TransportEnvelopeEncryption replacementEncryption) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport", target, "updateSecurity", new String[]{"SharedTransportEnvelopeAuthenticator", "TransportEnvelopeEncryption"}, new Object[]{replacementAuthenticator, replacementEncryption}); }
|
||||
public static void acceptAuthenticatedEnvelope(RedisBackendProxyTransport target, JsonEnvelope envelope, String channel) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport", target, "acceptAuthenticatedEnvelope", new String[]{"JsonEnvelope", "String"}, new Object[]{envelope, channel}); }
|
||||
public static void dispatchReceivedSubscriberEnvelope(RedisBackendProxyTransport target, JsonEnvelope envelope, String deliveryId) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport", target, "dispatchReceivedSubscriberEnvelope", new String[]{"JsonEnvelope", "String"}, new Object[]{envelope, deliveryId}); }
|
||||
public static void dispatchIdentified(RedisBackendProxyTransport target, JsonEnvelope envelope, String deliveryId) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport", target, "dispatchIdentified", new String[]{"JsonEnvelope", "String"}, new Object[]{envelope, deliveryId}); }
|
||||
public static void dispatchLegacy(RedisBackendProxyTransport target, JsonEnvelope envelope) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport", target, "dispatchLegacy", new String[]{"JsonEnvelope"}, new Object[]{envelope}); }
|
||||
public static void fenceAfterHandoff(RedisBackendProxyTransport target) { ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport", target, "fenceAfterHandoff", new String[]{}, new Object[]{}); }
|
||||
public static DefaultJedisClientConfig buildValidationClientConfig(int database, String username, String password, boolean ssl) { return (DefaultJedisClientConfig) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport", null, "buildValidationClientConfig", new String[]{"int", "String", "String", "boolean"}, new Object[]{database, username, password, ssl}); }
|
||||
}
|
||||
+81
-75
@@ -1,4 +1,10 @@
|
||||
package com.bencodez.votingplugin.backendproxy.transport;
|
||||
package com.bencodez.votingplugin.tests.backendproxy.transport;
|
||||
|
||||
import com.bencodez.votingplugin.tests.backendproxy.cache.ProcessedVoteCacheAccess;
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import com.bencodez.votingplugin.backendproxy.transport.BackendProxyTransportManager;
|
||||
import com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -50,17 +56,17 @@ class RedisBackendProxyTransportTest {
|
||||
SharedTransportEnvelopeAuthenticator authenticator = authenticator();
|
||||
TransportEnvelopeEncryption encryption = TransportEnvelopeEncryption.load(
|
||||
temporaryDirectory.resolve("secretkey.key"), TransportEnvelopeEncryption.Domain.PROXY_BACKEND, true);
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
setField(transport, "messageHandler", messages);
|
||||
setField(transport, "authenticator", authenticator);
|
||||
transport.updateSecurity(authenticator, encryption);
|
||||
RedisBackendProxyTransportAccess.updateSecurity(transport, authenticator, encryption);
|
||||
JsonEnvelope vote = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).put("player", "Alex").build();
|
||||
JsonEnvelope encrypted = VotingPluginWire.withRedisDeliveryId(encryption.encrypt(vote));
|
||||
JsonEnvelope signed = authenticator.sign(encrypted, Domain.REDIS_PROXY_BACKEND, "proxy-a",
|
||||
"VotingPlugin_backend-a");
|
||||
|
||||
transport.acceptAuthenticatedEnvelope(signed, "VotingPlugin_backend-a");
|
||||
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "VotingPlugin_backend-a");
|
||||
|
||||
verify(messages).onMessage(org.mockito.ArgumentMatchers.argThat(received ->
|
||||
VotingPluginWire.SUB_VOTE.equals(received.getSubChannel())
|
||||
@@ -70,7 +76,7 @@ class RedisBackendProxyTransportTest {
|
||||
@Test
|
||||
void authenticatedVoteIsAcceptedAndUnsignedPresenceIsRejectedBeforeDispatch() throws Exception {
|
||||
SharedTransportEnvelopeAuthenticator authenticator = authenticator();
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
setField(transport, "messageHandler", messages);
|
||||
setField(transport, "authenticator", authenticator);
|
||||
@@ -79,9 +85,9 @@ class RedisBackendProxyTransportTest {
|
||||
JsonEnvelope signedVote = authenticator.sign(VotingPluginWire.withRedisDeliveryId(vote),
|
||||
Domain.REDIS_PROXY_BACKEND, "proxy-a", "VotingPlugin_backend-a");
|
||||
|
||||
transport.acceptAuthenticatedEnvelope(signedVote, "VotingPlugin_backend-a");
|
||||
transport.acceptAuthenticatedEnvelope(vote, "VotingPlugin_backend-a");
|
||||
transport.acceptAuthenticatedEnvelope(VotingPluginWire.login("Alex",
|
||||
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signedVote, "VotingPlugin_backend-a");
|
||||
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, vote, "VotingPlugin_backend-a");
|
||||
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, VotingPluginWire.login("Alex",
|
||||
"00000000-0000-0000-0000-000000000001", "backend-a"), "VotingPlugin_backend-a");
|
||||
|
||||
verify(messages).onMessage(org.mockito.ArgumentMatchers.argThat(received ->
|
||||
@@ -94,7 +100,7 @@ class RedisBackendProxyTransportTest {
|
||||
@Test
|
||||
void copiedRedisVoteCannotAuthenticateOnAnotherBackendChannel() throws Exception {
|
||||
SharedTransportEnvelopeAuthenticator authenticator = authenticator();
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
setField(transport, "messageHandler", messages);
|
||||
setField(transport, "authenticator", authenticator);
|
||||
@@ -102,31 +108,31 @@ class RedisBackendProxyTransportTest {
|
||||
JsonEnvelope signed = authenticator.sign(vote, Domain.REDIS_PROXY_BACKEND, "proxy-a",
|
||||
"VotingPlugin_backend-a");
|
||||
|
||||
transport.acceptAuthenticatedEnvelope(signed, "VotingPlugin_backend-b");
|
||||
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "VotingPlugin_backend-b");
|
||||
verifyNoInteractions(messages);
|
||||
transport.acceptAuthenticatedEnvelope(signed, "VotingPlugin_backend-a");
|
||||
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "VotingPlugin_backend-a");
|
||||
verify(messages).onMessage(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void exactAuthenticatedRedisReplayCannotDuplicateVoteProcessing() throws Exception {
|
||||
SharedTransportEnvelopeAuthenticator authenticator = authenticator();
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
setField(transport, "messageHandler", messages);
|
||||
setField(transport, "authenticator", authenticator);
|
||||
JsonEnvelope signed = authenticator.sign(VotingPluginWire.withRedisDeliveryId(
|
||||
JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()), Domain.REDIS_PROXY_BACKEND, "proxy-a", "VotingPlugin_backend-a");
|
||||
|
||||
transport.acceptAuthenticatedEnvelope(signed, "VotingPlugin_backend-a");
|
||||
transport.acceptAuthenticatedEnvelope(signed, "VotingPlugin_backend-a");
|
||||
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "VotingPlugin_backend-a");
|
||||
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "VotingPlugin_backend-a");
|
||||
|
||||
verify(messages, times(1)).onMessage(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationHonorsTlsAndHostnameVerification() {
|
||||
DefaultJedisClientConfig config = RedisBackendProxyTransport.buildValidationClientConfig(3, "user", "secret",
|
||||
DefaultJedisClientConfig config = RedisBackendProxyTransportAccess.buildValidationClientConfig(3, "user", "secret",
|
||||
true);
|
||||
|
||||
assertTrue(config.isSsl());
|
||||
@@ -143,20 +149,20 @@ class RedisBackendProxyTransportTest {
|
||||
|
||||
@Test
|
||||
void validationKeepsTlsDisabledByDefault() {
|
||||
assertFalse(RedisBackendProxyTransport.buildValidationClientConfig(0, null, null, false).isSsl());
|
||||
assertFalse(RedisBackendProxyTransportAccess.buildValidationClientConfig(0, null, null, false).isSsl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyHandoffBufferDegradesAtItsFixedLimit() throws Exception {
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
|
||||
handler.setAccessible(true);
|
||||
handler.set(transport, messages);
|
||||
ReflectiveAccess.set(handler, transport, messages);
|
||||
JsonEnvelope envelope = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build();
|
||||
|
||||
for (int index = 0; index <= RedisBackendProxyTransport.MAX_LEGACY_HANDOFF_DELIVERIES; index++) {
|
||||
transport.dispatchLegacy(envelope);
|
||||
for (int index = 0; index <= RedisBackendProxyTransportAccess.MAX_LEGACY_HANDOFF_DELIVERIES; index++) {
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, envelope);
|
||||
}
|
||||
|
||||
verify(messages, times(1)).onMessage(envelope);
|
||||
@@ -164,45 +170,45 @@ class RedisBackendProxyTransportTest {
|
||||
|
||||
@Test
|
||||
void legacyHandoffBufferDegradesForAnOversizedEnvelope() throws Exception {
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
|
||||
handler.setAccessible(true);
|
||||
handler.set(transport, messages);
|
||||
ReflectiveAccess.set(handler, transport, messages);
|
||||
JsonEnvelope envelope = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE)
|
||||
.put("payload", "x".repeat(ProcessedVoteCache.MAX_LEGACY_REDIS_DELIVERY_BYTES + 1)).build();
|
||||
|
||||
transport.dispatchLegacy(envelope);
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, envelope);
|
||||
|
||||
verify(messages).onMessage(envelope);
|
||||
}
|
||||
|
||||
@Test
|
||||
void retiredListenerDropsLegacyDeliveriesAfterShutdownTimeout() throws Exception {
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
|
||||
handler.setAccessible(true);
|
||||
handler.set(transport, messages);
|
||||
transport.fenceAfterHandoff();
|
||||
ReflectiveAccess.set(handler, transport, messages);
|
||||
RedisBackendProxyTransportAccess.fenceAfterHandoff(transport);
|
||||
|
||||
transport.dispatchLegacy(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build());
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build());
|
||||
|
||||
verifyNoInteractions(messages);
|
||||
}
|
||||
|
||||
@Test
|
||||
void retiredListenerDoesNotReserveOrDispatchDeliveryIds() throws Exception {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache();
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, cache);
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
|
||||
handler.setAccessible(true);
|
||||
handler.set(transport, messages);
|
||||
transport.fenceAfterHandoff();
|
||||
ReflectiveAccess.set(handler, transport, messages);
|
||||
RedisBackendProxyTransportAccess.fenceAfterHandoff(transport);
|
||||
String deliveryId = "00000000-0000-0000-0000-000000000001";
|
||||
|
||||
transport.dispatchIdentified(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build(), deliveryId);
|
||||
RedisBackendProxyTransportAccess.dispatchIdentified(transport, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build(), deliveryId);
|
||||
|
||||
assertTrue(cache.reserveRedisDelivery(deliveryId), "retired listener must not consume shared deduplication state");
|
||||
verifyNoInteractions(messages);
|
||||
@@ -210,7 +216,7 @@ class RedisBackendProxyTransportTest {
|
||||
|
||||
@Test
|
||||
void standbyBuffersIdentifiedDeliveriesUntilItIsPublished() throws Exception {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache();
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
|
||||
cache.registerRedisSubscriber(new Object());
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, cache);
|
||||
Field identity = RedisBackendProxyTransport.class.getDeclaredField("subscriberIdentity");
|
||||
@@ -222,12 +228,12 @@ class RedisBackendProxyTransportTest {
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
|
||||
handler.setAccessible(true);
|
||||
handler.set(transport, messages);
|
||||
ReflectiveAccess.set(handler, transport, messages);
|
||||
String deliveryId = "00000000-0000-0000-0000-000000000002";
|
||||
JsonEnvelope envelope = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE)
|
||||
.put(VotingPluginWire.K_REDIS_DELIVERY_ID, deliveryId).build();
|
||||
|
||||
transport.dispatchIdentified(envelope, deliveryId);
|
||||
RedisBackendProxyTransportAccess.dispatchIdentified(transport, envelope, deliveryId);
|
||||
|
||||
verifyNoInteractions(messages);
|
||||
transport.activateAfterHandoff();
|
||||
@@ -252,8 +258,8 @@ class RedisBackendProxyTransportTest {
|
||||
JsonEnvelope identified = JsonEnvelope.builder("identified")
|
||||
.put(VotingPluginWire.K_REDIS_DELIVERY_ID, deliveryId).build();
|
||||
|
||||
transport.dispatchLegacy(legacy);
|
||||
transport.dispatchIdentified(identified, deliveryId);
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, legacy);
|
||||
RedisBackendProxyTransportAccess.dispatchIdentified(transport, identified, deliveryId);
|
||||
transport.activateAfterHandoff();
|
||||
transport.replayAfterHandoffPublication();
|
||||
|
||||
@@ -264,7 +270,7 @@ class RedisBackendProxyTransportTest {
|
||||
|
||||
@Test
|
||||
void legacyCopiesDoNotExposeReliableDeliveryIdentity() throws Exception {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache();
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, cache);
|
||||
Field identity = RedisBackendProxyTransport.class.getDeclaredField("subscriberIdentity");
|
||||
identity.setAccessible(true);
|
||||
@@ -275,7 +281,7 @@ class RedisBackendProxyTransportTest {
|
||||
JsonEnvelope mixed = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE)
|
||||
.put(VotingPluginWire.K_REDIS_DELIVERY_ID, deliveryId).put("payload", "legacy").build();
|
||||
|
||||
transport.dispatchLegacy(mixed);
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, mixed);
|
||||
|
||||
org.mockito.ArgumentCaptor<JsonEnvelope> captured = org.mockito.ArgumentCaptor.forClass(JsonEnvelope.class);
|
||||
verify(messages).onMessage(captured.capture());
|
||||
@@ -288,9 +294,9 @@ class RedisBackendProxyTransportTest {
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, mock(ProcessedVoteCache.class));
|
||||
setBoolean(transport, "standbySubscriber", true);
|
||||
setField(transport, "messageHandler", mock(GlobalMessageHandler.class));
|
||||
for (int index = 0; index <= RedisBackendProxyTransport.MAX_IDENTIFIED_HANDOFF_DELIVERIES; index++) {
|
||||
for (int index = 0; index <= RedisBackendProxyTransportAccess.MAX_IDENTIFIED_HANDOFF_DELIVERIES; index++) {
|
||||
String deliveryId = String.format("00000000-0000-0000-0000-%012d", index);
|
||||
transport.dispatchIdentified(JsonEnvelope.builder("identified")
|
||||
RedisBackendProxyTransportAccess.dispatchIdentified(transport, JsonEnvelope.builder("identified")
|
||||
.put(VotingPluginWire.K_REDIS_DELIVERY_ID, deliveryId).build(), deliveryId);
|
||||
}
|
||||
|
||||
@@ -306,7 +312,7 @@ class RedisBackendProxyTransportTest {
|
||||
JsonEnvelope oversized = JsonEnvelope.builder("legacy")
|
||||
.put("payload", "x".repeat(ProcessedVoteCache.MAX_LEGACY_REDIS_DELIVERY_BYTES + 1)).build();
|
||||
|
||||
transport.dispatchLegacy(oversized);
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, oversized);
|
||||
|
||||
verifyNoInteractions(messages);
|
||||
assertThrows(IllegalStateException.class, transport::activateAfterHandoff);
|
||||
@@ -319,8 +325,8 @@ class RedisBackendProxyTransportTest {
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
setField(transport, "messageHandler", messages);
|
||||
|
||||
for (int index = 0; index <= RedisBackendProxyTransport.MAX_LEGACY_HANDOFF_DELIVERIES; index++)
|
||||
transport.dispatchLegacy(JsonEnvelope.builder("legacy-" + index).build());
|
||||
for (int index = 0; index <= RedisBackendProxyTransportAccess.MAX_LEGACY_HANDOFF_DELIVERIES; index++)
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder("legacy-" + index).build());
|
||||
|
||||
verifyNoInteractions(messages);
|
||||
assertThrows(IllegalStateException.class, transport::activateAfterHandoff);
|
||||
@@ -328,7 +334,7 @@ class RedisBackendProxyTransportTest {
|
||||
|
||||
@Test
|
||||
void activeLegacyAccountingOverflowAbortsLaggingStandbyPublication() throws Exception {
|
||||
ProcessedVoteCache cache = new ProcessedVoteCache();
|
||||
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
|
||||
Object previousIdentity = new Object();
|
||||
cache.registerRedisSubscriber(previousIdentity);
|
||||
RedisBackendProxyTransport replacement = new RedisBackendProxyTransport(null, cache);
|
||||
@@ -346,7 +352,7 @@ class RedisBackendProxyTransportTest {
|
||||
|
||||
@Test
|
||||
void fencingWaitsForAnIdentifiedMessageHandler() throws Exception {
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
@@ -361,8 +367,8 @@ class RedisBackendProxyTransportTest {
|
||||
}).when(messages).onMessage(org.mockito.ArgumentMatchers.any(JsonEnvelope.class));
|
||||
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
|
||||
handler.setAccessible(true);
|
||||
handler.set(transport, messages);
|
||||
Thread delivery = new Thread(() -> transport.dispatchIdentified(
|
||||
ReflectiveAccess.set(handler, transport, messages);
|
||||
Thread delivery = new Thread(() -> RedisBackendProxyTransportAccess.dispatchIdentified(transport,
|
||||
JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build(), "delivery"));
|
||||
delivery.start();
|
||||
assertTrue(entered.await(1, TimeUnit.SECONDS));
|
||||
@@ -372,7 +378,7 @@ class RedisBackendProxyTransportTest {
|
||||
new java.util.concurrent.atomic.AtomicReference<>();
|
||||
Thread retirement = new Thread(() -> {
|
||||
try {
|
||||
transport.fenceAfterHandoff();
|
||||
RedisBackendProxyTransportAccess.fenceAfterHandoff(transport);
|
||||
} catch (Throwable failure) {
|
||||
fenceFailure.set(failure);
|
||||
} finally {
|
||||
@@ -416,7 +422,7 @@ class RedisBackendProxyTransportTest {
|
||||
}).when(messages).onMessage(org.mockito.ArgumentMatchers.any(JsonEnvelope.class));
|
||||
setField(transport, "messageHandler", messages);
|
||||
|
||||
Thread delivery = new Thread(() -> transport.dispatchLegacy(
|
||||
Thread delivery = new Thread(() -> RedisBackendProxyTransportAccess.dispatchLegacy(transport,
|
||||
JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build()));
|
||||
delivery.start();
|
||||
assertTrue(callbackEntered.await(1, TimeUnit.SECONDS));
|
||||
@@ -475,8 +481,8 @@ class RedisBackendProxyTransportTest {
|
||||
JsonEnvelope identified = JsonEnvelope.builder("identified-overlap").build();
|
||||
setBoolean(standby, "standbySubscriber", true);
|
||||
setField(standby, "messageHandler", mock(GlobalMessageHandler.class));
|
||||
standby.dispatchLegacy(legacy);
|
||||
standby.dispatchIdentified(identified, "00000000-0000-0000-0000-000000001594");
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(standby, legacy);
|
||||
RedisBackendProxyTransportAccess.dispatchIdentified(standby, identified, "00000000-0000-0000-0000-000000001594");
|
||||
org.mockito.Mockito.doThrow(new IllegalStateException("standby promotion rejected"))
|
||||
.when(standby).activateAfterHandoff();
|
||||
setField(previous, "transport", oldTransport);
|
||||
@@ -533,13 +539,13 @@ class RedisBackendProxyTransportTest {
|
||||
setField(replacement, "messageHandler", replacementMessages);
|
||||
JsonEnvelope envelope = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build();
|
||||
|
||||
Thread delivery = new Thread(() -> previous.dispatchLegacy(envelope));
|
||||
Thread delivery = new Thread(() -> RedisBackendProxyTransportAccess.dispatchLegacy(previous, envelope));
|
||||
delivery.start();
|
||||
assertTrue(entered.await(1, TimeUnit.SECONDS));
|
||||
replacement.dispatchLegacy(envelope);
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(replacement, envelope);
|
||||
CountDownLatch promoted = new CountDownLatch(1);
|
||||
Thread promotion = new Thread(() -> {
|
||||
previous.fenceAfterHandoff();
|
||||
RedisBackendProxyTransportAccess.fenceAfterHandoff(previous);
|
||||
replacement.activateAfterHandoff();
|
||||
promoted.countDown();
|
||||
});
|
||||
@@ -568,7 +574,7 @@ class RedisBackendProxyTransportTest {
|
||||
JsonEnvelope envelope = invocation.getArgument(0);
|
||||
order.add(envelope);
|
||||
if (envelope == replayed) {
|
||||
Thread arrival = new Thread(() -> transport.dispatchIdentified(newer, "new-delivery"));
|
||||
Thread arrival = new Thread(() -> RedisBackendProxyTransportAccess.dispatchIdentified(transport, newer, "new-delivery"));
|
||||
arrival.start();
|
||||
arrival.join(TimeUnit.SECONDS.toMillis(1));
|
||||
}
|
||||
@@ -576,8 +582,8 @@ class RedisBackendProxyTransportTest {
|
||||
}).when(messages).onMessage(org.mockito.ArgumentMatchers.any(JsonEnvelope.class));
|
||||
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
|
||||
handler.setAccessible(true);
|
||||
handler.set(transport, messages);
|
||||
transport.dispatchLegacy(replayed);
|
||||
ReflectiveAccess.set(handler, transport, messages);
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, replayed);
|
||||
|
||||
transport.activateAfterHandoff();
|
||||
transport.replayAfterHandoffPublication();
|
||||
@@ -601,15 +607,15 @@ class RedisBackendProxyTransportTest {
|
||||
when(cache.consumeLegacyRedisDelivery(org.mockito.ArgumentMatchers.anyString())).thenReturn(false);
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(plugin, cache);
|
||||
setBoolean(transport, "standbySubscriber", true);
|
||||
CountDownLatch replayed = new CountDownLatch(RedisBackendProxyTransport.REPLAY_BATCH_SIZE + 1);
|
||||
CountDownLatch replayed = new CountDownLatch(RedisBackendProxyTransportAccess.REPLAY_BATCH_SIZE + 1);
|
||||
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
|
||||
doAnswer(invocation -> {
|
||||
replayed.countDown();
|
||||
return null;
|
||||
}).when(messages).onMessage(org.mockito.ArgumentMatchers.any(JsonEnvelope.class));
|
||||
setField(transport, "messageHandler", messages);
|
||||
for (int index = 0; index <= RedisBackendProxyTransport.REPLAY_BATCH_SIZE; index++)
|
||||
transport.dispatchLegacy(JsonEnvelope.builder("replay-" + index).build());
|
||||
for (int index = 0; index <= RedisBackendProxyTransportAccess.REPLAY_BATCH_SIZE; index++)
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder("replay-" + index).build());
|
||||
|
||||
transport.activateAfterHandoff();
|
||||
transport.replayAfterHandoffPublication();
|
||||
@@ -630,14 +636,14 @@ class RedisBackendProxyTransportTest {
|
||||
queueField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.ArrayDeque<JsonEnvelope> queue = (java.util.ArrayDeque<JsonEnvelope>) queueField.get(transport);
|
||||
for (int index = 0; index < RedisBackendProxyTransport.MAX_REPLAY_HANDOFF_DELIVERIES; index++)
|
||||
for (int index = 0; index < RedisBackendProxyTransportAccess.MAX_REPLAY_HANDOFF_DELIVERIES; index++)
|
||||
queue.addLast(JsonEnvelope.builder("queued-" + index).build());
|
||||
JsonEnvelope newer = JsonEnvelope.builder("new").build();
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch returned = new CountDownLatch(1);
|
||||
Thread callback = new Thread(() -> {
|
||||
entered.countDown();
|
||||
transport.dispatchIdentified(newer, "new-delivery");
|
||||
RedisBackendProxyTransportAccess.dispatchIdentified(transport, newer, "new-delivery");
|
||||
returned.countDown();
|
||||
});
|
||||
callback.start();
|
||||
@@ -662,12 +668,12 @@ class RedisBackendProxyTransportTest {
|
||||
setField(transport, "messageHandler", mock(GlobalMessageHandler.class));
|
||||
setBoolean(transport, "replayingHandoff", true);
|
||||
java.util.ArrayDeque<JsonEnvelope> queue = replayQueue(transport);
|
||||
for (int index = 0; index < RedisBackendProxyTransport.MAX_REPLAY_HANDOFF_DELIVERIES; index++)
|
||||
for (int index = 0; index < RedisBackendProxyTransportAccess.MAX_REPLAY_HANDOFF_DELIVERIES; index++)
|
||||
queue.addLast(JsonEnvelope.builder("queued-" + index).build());
|
||||
JsonEnvelope received = JsonEnvelope.builder("received").build();
|
||||
CountDownLatch completed = new CountDownLatch(1);
|
||||
Thread subscriber = new Thread(() -> {
|
||||
transport.dispatchReceivedSubscriberEnvelope(received, "received-delivery");
|
||||
RedisBackendProxyTransportAccess.dispatchReceivedSubscriberEnvelope(transport, received, "received-delivery");
|
||||
completed.countDown();
|
||||
});
|
||||
subscriber.start();
|
||||
@@ -700,11 +706,11 @@ class RedisBackendProxyTransportTest {
|
||||
JsonEnvelope before = JsonEnvelope.builder("before").build();
|
||||
JsonEnvelope during = JsonEnvelope.builder("during").build();
|
||||
|
||||
replacement.dispatchIdentified(before, "before-publication");
|
||||
RedisBackendProxyTransportAccess.dispatchIdentified(replacement, before, "before-publication");
|
||||
replacement.activateAfterHandoff();
|
||||
// This is the narrow interval after worker-side promotion but before Bukkit
|
||||
// opens publication/replay. It has already reserved its Redis identity.
|
||||
replacement.dispatchIdentified(during, "during-publication");
|
||||
RedisBackendProxyTransportAccess.dispatchIdentified(replacement, during, "during-publication");
|
||||
|
||||
assertEquals(java.util.List.of(before, during), replacement.detachReplayForFailedHandoff());
|
||||
assertTrue(replayQueue(replacement).isEmpty());
|
||||
@@ -734,7 +740,7 @@ class RedisBackendProxyTransportTest {
|
||||
setField(replacement, "messageHandler", messages);
|
||||
replacement.acceptReplayFromPreviousHandoff(transferred);
|
||||
JsonEnvelope newer = JsonEnvelope.builder("newer").build();
|
||||
replacement.dispatchIdentified(newer, "new-overlap");
|
||||
RedisBackendProxyTransportAccess.dispatchIdentified(replacement, newer, "new-overlap");
|
||||
replacement.activateAfterHandoff();
|
||||
replacement.replayAfterHandoffPublication();
|
||||
|
||||
@@ -754,7 +760,7 @@ class RedisBackendProxyTransportTest {
|
||||
JsonEnvelope duringFreeze = JsonEnvelope.builder("during-freeze").build();
|
||||
CountDownLatch completed = new CountDownLatch(1);
|
||||
Thread callback = new Thread(() -> {
|
||||
previous.dispatchIdentified(duringFreeze, "during-freeze");
|
||||
RedisBackendProxyTransportAccess.dispatchIdentified(previous, duringFreeze, "during-freeze");
|
||||
completed.countDown();
|
||||
});
|
||||
callback.start();
|
||||
@@ -778,7 +784,7 @@ class RedisBackendProxyTransportTest {
|
||||
previous.freezeReplayForSuccessiveHandoff();
|
||||
CountDownLatch callbackDone = new CountDownLatch(1);
|
||||
Thread callback = new Thread(() -> {
|
||||
previous.dispatchIdentified(JsonEnvelope.builder("during-close").build(), "during-close");
|
||||
RedisBackendProxyTransportAccess.dispatchIdentified(previous, JsonEnvelope.builder( "during-close").build(), "during-close");
|
||||
callbackDone.countDown();
|
||||
});
|
||||
callback.start();
|
||||
@@ -823,7 +829,7 @@ class RedisBackendProxyTransportTest {
|
||||
|
||||
@Test
|
||||
void retiredRedisCleanupDoesNotBlockManagerCloseOnPublicationThread() throws Exception {
|
||||
BackendProxyTransportManager manager = new BackendProxyTransportManager(null, new ProcessedVoteCache());
|
||||
BackendProxyTransportManager manager = new BackendProxyTransportManager(null, ProcessedVoteCacheAccess.create());
|
||||
RedisBackendProxyTransport retired = mock(RedisBackendProxyTransport.class);
|
||||
CountDownLatch closeEntered = new CountDownLatch(1);
|
||||
CountDownLatch releaseClose = new CountDownLatch(1);
|
||||
@@ -860,7 +866,7 @@ class RedisBackendProxyTransportTest {
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(plugin, cache);
|
||||
setBoolean(transport, "standbySubscriber", true);
|
||||
setField(transport, "messageHandler", mock(GlobalMessageHandler.class));
|
||||
transport.dispatchLegacy(JsonEnvelope.builder("replay").build());
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder("replay").build());
|
||||
transport.activateAfterHandoff();
|
||||
transport.replayAfterHandoffPublication();
|
||||
|
||||
@@ -889,7 +895,7 @@ class RedisBackendProxyTransportTest {
|
||||
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(plugin, cache);
|
||||
setBoolean(transport, "standbySubscriber", true);
|
||||
setField(transport, "messageHandler", mock(GlobalMessageHandler.class));
|
||||
transport.dispatchLegacy(JsonEnvelope.builder("replay").build());
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder("replay").build());
|
||||
transport.activateAfterHandoff();
|
||||
transport.replayAfterHandoffPublication();
|
||||
assertTrue(submitted.await(1, TimeUnit.SECONDS));
|
||||
@@ -930,8 +936,8 @@ class RedisBackendProxyTransportTest {
|
||||
return null;
|
||||
}).when(messages).onMessage(org.mockito.ArgumentMatchers.any(JsonEnvelope.class));
|
||||
setField(transport, "messageHandler", messages);
|
||||
transport.dispatchLegacy(JsonEnvelope.builder("first").build());
|
||||
transport.dispatchLegacy(JsonEnvelope.builder("second").build());
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder("first").build());
|
||||
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder("second").build());
|
||||
transport.activateAfterHandoff();
|
||||
transport.replayAfterHandoffPublication();
|
||||
assertTrue(entered.await(1, TimeUnit.SECONDS));
|
||||
@@ -961,7 +967,7 @@ class RedisBackendProxyTransportTest {
|
||||
private static void setField(Object target, String name, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
ReflectiveAccess.set(field, target, value);
|
||||
}
|
||||
|
||||
private static java.util.List<JsonEnvelope> preparedSendQueue(BackendProxyTransportManager manager)
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.bencodez.votingplugin.tests.backendproxy.transport;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.backendproxy.transport.*;
|
||||
import com.bencodez.votingplugin.proxy.security.SharedTransportEnvelopeAuthenticator;
|
||||
import com.bencodez.votingplugin.proxy.security.TransportEnvelopeEncryption;
|
||||
|
||||
/** Test-only forwarding access to the non-public production SharedInboundPolicy. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class SharedInboundPolicyAccess implements ReflectiveAccess.View {
|
||||
private final Object delegate;
|
||||
private SharedInboundPolicyAccess(Object delegate) { this.delegate = delegate; }
|
||||
public static SharedInboundPolicyAccess view(Object delegate) { return delegate == null ? null : new SharedInboundPolicyAccess(delegate); }
|
||||
@Override public Object delegate() { return delegate; }
|
||||
@Override public boolean equals(Object other) { return other instanceof SharedInboundPolicyAccess value && delegate.equals(value.delegate); }
|
||||
@Override public int hashCode() { return delegate.hashCode(); }
|
||||
public static SharedInboundPolicyAccess mock() { return view(org.mockito.Mockito.mock(ReflectiveAccess.type("com.bencodez.votingplugin.backendproxy.transport.SharedInboundPolicy"))); }
|
||||
public SharedInboundPolicyAccess verify() { return view(org.mockito.Mockito.verify(delegate)); }
|
||||
public SharedInboundPolicyAccess verify(org.mockito.verification.VerificationMode mode) { return view(org.mockito.Mockito.verify(delegate, mode)); }
|
||||
public SharedInboundPolicyAccess stub(org.mockito.stubbing.Stubber stubber) { return view(stubber.when(delegate)); }
|
||||
public SharedInboundPolicyAccess(Class<? extends BackendProxyTransport> transportType, String destination, SharedTransportEnvelopeAuthenticator authenticator, TransportEnvelopeEncryption encryption) { this(ReflectiveAccess.construct("com.bencodez.votingplugin.backendproxy.transport.SharedInboundPolicy", new String[]{"Class", "String", "SharedTransportEnvelopeAuthenticator", "TransportEnvelopeEncryption"}, new Object[]{transportType, destination, authenticator, encryption})); }
|
||||
public Class<? extends BackendProxyTransport> transportType() { return (Class<? extends BackendProxyTransport>) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.SharedInboundPolicy", delegate, "transportType", new String[]{}, new Object[]{}); }
|
||||
public String destination() { return (String) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.SharedInboundPolicy", delegate, "destination", new String[]{}, new Object[]{}); }
|
||||
public SharedTransportEnvelopeAuthenticator authenticator() { return (SharedTransportEnvelopeAuthenticator) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.SharedInboundPolicy", delegate, "authenticator", new String[]{}, new Object[]{}); }
|
||||
public TransportEnvelopeEncryption encryption() { return (TransportEnvelopeEncryption) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.SharedInboundPolicy", delegate, "encryption", new String[]{}, new Object[]{}); }
|
||||
public boolean hasEquivalentPolicy(SharedInboundPolicyAccess other) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.backendproxy.transport.SharedInboundPolicy", delegate, "hasEquivalentPolicy", new String[]{"SharedInboundPolicy"}, new Object[]{other}); }
|
||||
}
|
||||
+11
-7
@@ -1,4 +1,8 @@
|
||||
package com.bencodez.votingplugin.backendproxy.transport;
|
||||
package com.bencodez.votingplugin.tests.backendproxy.transport;
|
||||
|
||||
import com.bencodez.votingplugin.backendproxy.transport.MqttBackendProxyTransport;
|
||||
import com.bencodez.votingplugin.backendproxy.transport.RedisBackendProxyTransport;
|
||||
import com.bencodez.votingplugin.tests.backendproxy.transport.SharedInboundPolicyAccess;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -24,19 +28,19 @@ class SharedInboundPolicyTest {
|
||||
SharedTransportEnvelopeAuthenticator required = SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED);
|
||||
TransportEnvelopeEncryption encrypted = TransportEnvelopeEncryption.load(keyFile,
|
||||
TransportEnvelopeEncryption.Domain.PROXY_BACKEND, true);
|
||||
SharedInboundPolicy policy = new SharedInboundPolicy(RedisBackendProxyTransport.class, "vp:a", required,
|
||||
SharedInboundPolicyAccess policy = new SharedInboundPolicyAccess(RedisBackendProxyTransport.class, "vp:a", required,
|
||||
encrypted);
|
||||
|
||||
assertTrue(policy.hasEquivalentPolicy(new SharedInboundPolicy(RedisBackendProxyTransport.class, "vp:a",
|
||||
assertTrue(policy.hasEquivalentPolicy(new SharedInboundPolicyAccess(RedisBackendProxyTransport.class, "vp:a",
|
||||
SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED), TransportEnvelopeEncryption.load(keyFile,
|
||||
TransportEnvelopeEncryption.Domain.PROXY_BACKEND, true))));
|
||||
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicy(RedisBackendProxyTransport.class, "vp:b",
|
||||
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicyAccess(RedisBackendProxyTransport.class, "vp:b",
|
||||
SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED), encrypted)));
|
||||
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicy(MqttBackendProxyTransport.class, "vp:a",
|
||||
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicyAccess(MqttBackendProxyTransport.class, "vp:a",
|
||||
SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED), encrypted)));
|
||||
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicy(RedisBackendProxyTransport.class, "vp:a",
|
||||
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicyAccess(RedisBackendProxyTransport.class, "vp:a",
|
||||
SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.COMPATIBILITY), encrypted)));
|
||||
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicy(RedisBackendProxyTransport.class, "vp:a",
|
||||
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicyAccess(RedisBackendProxyTransport.class, "vp:a",
|
||||
SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED), TransportEnvelopeEncryption.load(keyFile,
|
||||
TransportEnvelopeEncryption.Domain.PROXY_BACKEND, false))));
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.bencodez.votingplugin.tests.backgroundtask;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.backgroundtask.*;
|
||||
import com.bencodez.votingplugin.backgroundtask.VotingPluginBackgroundTask.*;
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
|
||||
/** Typed test-only access to package-private VotingPluginBackgroundTask seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class VotingPluginBackgroundTaskAccess {
|
||||
private VotingPluginBackgroundTaskAccess() { }
|
||||
public static VotingPluginBackgroundTask create(VotingPluginMain plugin) { return (VotingPluginBackgroundTask) ReflectiveAccess.construct("com.bencodez.votingplugin.backgroundtask.VotingPluginBackgroundTask", new String[]{"VotingPluginMain"}, new Object[]{plugin}); }
|
||||
public static VotingPluginBackgroundTask create(VotingPluginMain plugin, long snapshotWaitMillis) { return (VotingPluginBackgroundTask) ReflectiveAccess.construct("com.bencodez.votingplugin.backgroundtask.VotingPluginBackgroundTask", new String[]{"VotingPluginMain", "long"}, new Object[]{plugin, snapshotWaitMillis}); }
|
||||
}
|
||||
+9
-7
@@ -1,6 +1,8 @@
|
||||
package com.bencodez.votingplugin.tests.backgroundtask;
|
||||
|
||||
import com.bencodez.votingplugin.util.EntityTaskResultTestCompat;
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import com.bencodez.votingplugin.tests.util.EntityTaskResultTestCompat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -28,7 +30,7 @@ public class VotingPluginBackgroundTaskTest {
|
||||
|
||||
@Test
|
||||
public void requestedStateCanBeDelegated() {
|
||||
VotingPluginBackgroundTask task = new VotingPluginBackgroundTask(mock(VotingPluginMain.class));
|
||||
VotingPluginBackgroundTask task = VotingPluginBackgroundTaskAccess.create(mock(VotingPluginMain.class));
|
||||
assertFalse(task.isRequested());
|
||||
task.setRequested(true);
|
||||
assertTrue(task.isRequested());
|
||||
@@ -46,7 +48,7 @@ public class VotingPluginBackgroundTaskTest {
|
||||
new java.util.concurrent.atomic.AtomicReference<>();
|
||||
org.mockito.Mockito.doAnswer(call -> { callback.set(call.getArgument(0)); return null; })
|
||||
.when(plugin).captureOnlineTopVoterIgnore(any(), any());
|
||||
VotingPluginBackgroundTask task = new VotingPluginBackgroundTask(plugin);
|
||||
VotingPluginBackgroundTask task = VotingPluginBackgroundTaskAccess.create(plugin);
|
||||
task.setRequested(true);
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||
@@ -74,7 +76,7 @@ public class VotingPluginBackgroundTaskTest {
|
||||
.when(plugin).captureOnlineTopVoterIgnore(any(), any());
|
||||
java.util.concurrent.ScheduledExecutorService storage = mock(java.util.concurrent.ScheduledExecutorService.class);
|
||||
when(plugin.getUserManager().getDataManager().getTimer()).thenReturn(storage);
|
||||
VotingPluginBackgroundTask task = new VotingPluginBackgroundTask(plugin);
|
||||
VotingPluginBackgroundTask task = VotingPluginBackgroundTaskAccess.create(plugin);
|
||||
task.setRequested(true);
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||
@@ -124,7 +126,7 @@ public class VotingPluginBackgroundTaskTest {
|
||||
java.lang.reflect.Constructor<VotingPluginBackgroundTask> constructor =
|
||||
VotingPluginBackgroundTask.class.getDeclaredConstructor(VotingPluginMain.class, long.class);
|
||||
constructor.setAccessible(true);
|
||||
VotingPluginBackgroundTask task = constructor.newInstance(plugin, 10L);
|
||||
VotingPluginBackgroundTask task = ReflectiveAccess.newInstance(constructor, new Object[]{plugin, 10L});
|
||||
task.setRequested(true);
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||
@@ -147,7 +149,7 @@ public class VotingPluginBackgroundTaskTest {
|
||||
when(plugin.getConfigFile()).thenReturn(config);
|
||||
doAnswer(call -> { call.getArgument(1, Runnable.class).run(); return null; })
|
||||
.when(plugin).captureOnlineTopVoterIgnore(any(), any());
|
||||
VotingPluginBackgroundTask task = new VotingPluginBackgroundTask(plugin);
|
||||
VotingPluginBackgroundTask task = VotingPluginBackgroundTaskAccess.create(plugin);
|
||||
task.setRequested(true);
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||
@@ -174,7 +176,7 @@ public class VotingPluginBackgroundTaskTest {
|
||||
java.lang.reflect.Constructor<VotingPluginBackgroundTask> constructor =
|
||||
VotingPluginBackgroundTask.class.getDeclaredConstructor(VotingPluginMain.class, long.class);
|
||||
constructor.setAccessible(true);
|
||||
VotingPluginBackgroundTask task = constructor.newInstance(plugin, 10L);
|
||||
VotingPluginBackgroundTask task = ReflectiveAccess.newInstance(constructor, new Object[]{plugin, 10L});
|
||||
task.setRequested(true);
|
||||
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.bencodez.votingplugin.build;
|
||||
package com.bencodez.votingplugin.tests.build;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
package com.bencodez.votingplugin.commands;
|
||||
package com.bencodez.votingplugin.tests.commands;
|
||||
|
||||
import com.bencodez.votingplugin.commands.AdminAuthorization;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.bencodez.votingplugin.tests.commands;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.commands.*;
|
||||
import com.bencodez.votingplugin.commands.CommandLoader.*;
|
||||
import java.util.List;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
import com.bencodez.advancedcore.api.command.CommandHandler;
|
||||
import com.bencodez.advancedcore.api.command.PlayerCommandHandler;
|
||||
import com.bencodez.votingplugin.user.VotingPluginUser;
|
||||
import com.bencodez.votingplugin.user.PointTransferResult;
|
||||
|
||||
/** Typed test-only access to package-private CommandLoader seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class CommandLoaderAccess {
|
||||
public static void registerOfflineVoteClearCommands(CommandLoader target, String adminPerm) { ReflectiveAccess.call("com.bencodez.votingplugin.commands.CommandLoader", target, "registerOfflineVoteClearCommands", new String[]{"String"}, new Object[]{adminPerm}); }
|
||||
public static void runConfirmedBulkStorageMutation(CommandLoader target, CommandSender sender, java.util.function.BooleanSupplier mutation, Runnable success) { ReflectiveAccess.call("com.bencodez.votingplugin.commands.CommandLoader", target, "runConfirmedBulkStorageMutation", new String[]{"CommandSender", "BooleanSupplier", "Runnable"}, new Object[]{sender, mutation, success}); }
|
||||
private CommandLoaderAccess() { }
|
||||
public static void runForCommandSender(CommandLoader target, CommandSender sender, Runnable task) { ReflectiveAccess.call("com.bencodez.votingplugin.commands.CommandLoader", target, "runForCommandSender", new String[]{"CommandSender", "Runnable"}, new Object[]{sender, task}); }
|
||||
public static void runForVotingUser(CommandLoader target, VotingPluginUser user, Runnable task) { ReflectiveAccess.call("com.bencodez.votingplugin.commands.CommandLoader", target, "runForVotingUser", new String[]{"VotingPluginUser", "Runnable"}, new Object[]{user, task}); }
|
||||
|
||||
public static void runBulkStorageMutation(CommandLoader target, CommandSender sender, Runnable mutation, Runnable success) { ReflectiveAccess.call("com.bencodez.votingplugin.commands.CommandLoader", target, "runBulkStorageMutation", new String[]{"CommandSender", "Runnable", "Runnable"}, new Object[]{sender, mutation, success}); }
|
||||
|
||||
public static void loadAllVotingUsersAsync(CommandLoader target, CommandSender sender, java.util.function.Consumer<CommandLoaderAccess.BulkVotingUsers> success) { ReflectiveAccess.call("com.bencodez.votingplugin.commands.CommandLoader", target, "loadAllVotingUsersAsync", new String[]{"CommandSender", "Consumer"}, new Object[]{sender, (java.util.function.Consumer<Object>) value -> success.accept(BulkVotingUsers.view(value))}); }
|
||||
public static String transferFailureMessage(CommandLoader target, PointTransferResult result) { return (String) ReflectiveAccess.call("com.bencodez.votingplugin.commands.CommandLoader", target, "transferFailureMessage", new String[]{"PointTransferResult"}, new Object[]{result}); }
|
||||
public static PlayerCommandHandler configureAllPermissionOverride(PlayerCommandHandler handler, String adminPermission) { return (PlayerCommandHandler) ReflectiveAccess.call("com.bencodez.votingplugin.commands.CommandLoader", null, "configureAllPermissionOverride", new String[]{"PlayerCommandHandler", "String"}, new Object[]{handler, adminPermission}); }
|
||||
public static void registerAdditionalPermissions(PluginManager manager, CommandHandler handler) { ReflectiveAccess.call("com.bencodez.votingplugin.commands.CommandLoader", null, "registerAdditionalPermissions", new String[]{"PluginManager", "CommandHandler"}, new Object[]{manager, handler}); }
|
||||
public static final class BulkVotingUsers implements ReflectiveAccess.View {
|
||||
|
||||
private final Object delegate;
|
||||
private BulkVotingUsers(Object delegate) { this.delegate = delegate; }
|
||||
public static BulkVotingUsers view(Object delegate) { return delegate == null ? null : new BulkVotingUsers(delegate); }
|
||||
@Override public Object delegate() { return delegate; }
|
||||
@Override public boolean equals(Object other) { return other instanceof BulkVotingUsers value && delegate.equals(value.delegate); }
|
||||
@Override public int hashCode() { return delegate.hashCode(); }
|
||||
|
||||
public java.util.List<VotingPluginUser> users() { return (java.util.List<VotingPluginUser>) ReflectiveAccess.call("com.bencodez.votingplugin.commands.CommandLoader$BulkVotingUsers", delegate, "users", new String[]{}, new Object[]{}); }
|
||||
public java.util.IdentityHashMap<VotingPluginUser, Player> players() { return (java.util.IdentityHashMap<VotingPluginUser, Player>) ReflectiveAccess.call("com.bencodez.votingplugin.commands.CommandLoader$BulkVotingUsers", delegate, "players", new String[]{}, new Object[]{}); }
|
||||
public Player player(VotingPluginUser user) { return (Player) ReflectiveAccess.call("com.bencodez.votingplugin.commands.CommandLoader$BulkVotingUsers", delegate, "player", new String[]{"VotingPluginUser"}, new Object[]{user}); }
|
||||
}
|
||||
|
||||
}
|
||||
+7
-4
@@ -1,4 +1,7 @@
|
||||
package com.bencodez.votingplugin.commands;
|
||||
package com.bencodez.votingplugin.tests.commands;
|
||||
|
||||
import com.bencodez.votingplugin.commands.AdminAuthorization;
|
||||
import com.bencodez.votingplugin.commands.CommandLoader;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
@@ -26,12 +29,12 @@ class CommandLoaderBulkPermissionTest {
|
||||
PlayerCommandHandler handler = mock(PlayerCommandHandler.class);
|
||||
try {
|
||||
PlayerCommandHandler.class.getMethod("withAllPermissionOverrides", String[].class);
|
||||
assertSame(handler, CommandLoader.configureAllPermissionOverride(handler,
|
||||
assertSame(handler, CommandLoaderAccess.configureAllPermissionOverride(handler,
|
||||
AdminAuthorization.ADMIN_PERMISSION));
|
||||
assertTrue(mockingDetails(handler).getInvocations().stream()
|
||||
.anyMatch(invocation -> invocation.getMethod().getName().equals("withAllPermissionOverrides")));
|
||||
} catch (NoSuchMethodException exception) {
|
||||
assertThrows(IllegalStateException.class, () -> CommandLoader.configureAllPermissionOverride(handler,
|
||||
assertThrows(IllegalStateException.class, () -> CommandLoaderAccess.configureAllPermissionOverride(handler,
|
||||
AdminAuthorization.ADMIN_PERMISSION));
|
||||
}
|
||||
}
|
||||
@@ -44,7 +47,7 @@ class CommandLoaderBulkPermissionTest {
|
||||
.thenReturn(List.of("VotingPlugin.Commands.AdminVote.SetPoints.All"));
|
||||
when(manager.getPermission("VotingPlugin.Commands.AdminVote.SetPoints.All")).thenReturn(null);
|
||||
|
||||
CommandLoader.registerAdditionalPermissions(manager, handler);
|
||||
CommandLoaderAccess.registerAdditionalPermissions(manager, handler);
|
||||
|
||||
ArgumentCaptor<Permission> registered = ArgumentCaptor.forClass(Permission.class);
|
||||
verify(manager).addPermission(registered.capture());
|
||||
+18
-16
@@ -1,6 +1,8 @@
|
||||
package com.bencodez.votingplugin.commands;
|
||||
package com.bencodez.votingplugin.tests.commands;
|
||||
|
||||
import com.bencodez.votingplugin.util.EntityTaskResultTestCompat;
|
||||
import com.bencodez.votingplugin.commands.CommandLoader;
|
||||
|
||||
import com.bencodez.votingplugin.tests.util.EntityTaskResultTestCompat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -37,7 +39,7 @@ class CommandLoaderSchedulingTest {
|
||||
Player sender = mock(Player.class);
|
||||
Runnable completion = () -> { };
|
||||
|
||||
new CommandLoader(plugin).runForCommandSender(sender, completion);
|
||||
CommandLoaderAccess.runForCommandSender(new CommandLoader(plugin), sender, completion);
|
||||
|
||||
verify(scheduler).runTask(eq(plugin), any(Runnable.class), eq(sender));
|
||||
verify(scheduler, never()).runTask(eq(plugin), any(Runnable.class));
|
||||
@@ -52,7 +54,7 @@ class CommandLoaderSchedulingTest {
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
Runnable completion = () -> { };
|
||||
|
||||
new CommandLoader(plugin).runForCommandSender(sender, completion);
|
||||
CommandLoaderAccess.runForCommandSender(new CommandLoader(plugin), sender, completion);
|
||||
|
||||
verify(scheduler).runTask(eq(plugin), any(Runnable.class));
|
||||
verify(scheduler, never()).runTask(eq(plugin), any(Runnable.class), any(Player.class));
|
||||
@@ -69,7 +71,7 @@ class CommandLoaderSchedulingTest {
|
||||
when(user.getPlayer()).thenReturn(recipient);
|
||||
Runnable completion = () -> { };
|
||||
|
||||
new CommandLoader(plugin).runForVotingUser(user, completion);
|
||||
CommandLoaderAccess.runForVotingUser(new CommandLoader(plugin), user, completion);
|
||||
|
||||
verify(scheduler).runTask(eq(plugin), any(Runnable.class), eq(recipient));
|
||||
verify(scheduler, never()).runTask(eq(plugin), any(Runnable.class));
|
||||
@@ -83,7 +85,7 @@ class CommandLoaderSchedulingTest {
|
||||
VotingPluginUser user = mock(VotingPluginUser.class);
|
||||
Runnable completion = () -> { };
|
||||
|
||||
new CommandLoader(plugin).runForVotingUser(user, completion);
|
||||
CommandLoaderAccess.runForVotingUser(new CommandLoader(plugin), user, completion);
|
||||
|
||||
verify(scheduler).runTask(eq(plugin), any(Runnable.class));
|
||||
verify(scheduler, never()).runTask(eq(plugin), any(Runnable.class), any(Player.class));
|
||||
@@ -100,11 +102,11 @@ class CommandLoaderSchedulingTest {
|
||||
CommandLoader loader = new CommandLoader(plugin);
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertEquals("insufficient",
|
||||
loader.transferFailureMessage(PointTransferResult.INSUFFICIENT_POINTS));
|
||||
org.junit.jupiter.api.Assertions.assertEquals("retry", loader.transferFailureMessage(PointTransferResult.CANCELLED));
|
||||
org.junit.jupiter.api.Assertions.assertEquals("retry", loader.transferFailureMessage(PointTransferResult.UNAVAILABLE));
|
||||
CommandLoaderAccess.transferFailureMessage(loader, PointTransferResult.INSUFFICIENT_POINTS));
|
||||
org.junit.jupiter.api.Assertions.assertEquals("retry", CommandLoaderAccess.transferFailureMessage(loader, PointTransferResult.CANCELLED));
|
||||
org.junit.jupiter.api.Assertions.assertEquals("retry", CommandLoaderAccess.transferFailureMessage(loader, PointTransferResult.UNAVAILABLE));
|
||||
org.junit.jupiter.api.Assertions.assertEquals("pending; do not retry",
|
||||
loader.transferFailureMessage(PointTransferResult.PENDING_CONFIRMATION));
|
||||
CommandLoaderAccess.transferFailureMessage(loader, PointTransferResult.PENDING_CONFIRMATION));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -135,7 +137,7 @@ class CommandLoaderSchedulingTest {
|
||||
.when(storage).execute(any(Runnable.class));
|
||||
AtomicBoolean storageRan = new AtomicBoolean();
|
||||
|
||||
new CommandLoader(plugin).runBulkStorageMutation(sender, () -> storageRan.set(true), () -> { });
|
||||
CommandLoaderAccess.runBulkStorageMutation(new CommandLoader(plugin), sender, () -> storageRan.set(true), () -> { });
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertFalse(storageRan.get());
|
||||
org.junit.jupiter.api.Assertions.assertNotNull(worker.get());
|
||||
@@ -159,13 +161,13 @@ class CommandLoaderSchedulingTest {
|
||||
when(user.getPlayer()).thenReturn(player);
|
||||
when(plugin.getUserManager().getAllUUIDs())
|
||||
.thenReturn(new java.util.ArrayList<>(java.util.List.of(uuid.toString())));
|
||||
when(plugin.getVotingPluginUserManager().getVotingPluginUser(uuid)).thenReturn(user);
|
||||
when(plugin.getVotingPluginUserManager().getVotingPluginUser(uuid, true)).thenReturn(user);
|
||||
AtomicReference<Runnable> platform = new AtomicReference<>();
|
||||
org.mockito.Mockito.doAnswer(call -> { platform.set(call.getArgument(1)); return null; })
|
||||
.when(scheduler).runTask(eq(plugin), any(Runnable.class));
|
||||
AtomicReference<CommandLoader.BulkVotingUsers> delivered = new AtomicReference<>();
|
||||
AtomicReference<CommandLoaderAccess.BulkVotingUsers> delivered = new AtomicReference<>();
|
||||
|
||||
new CommandLoader(plugin).loadAllVotingUsersAsync(sender, delivered::set);
|
||||
CommandLoaderAccess.loadAllVotingUsersAsync(new CommandLoader(plugin), sender, delivered::set);
|
||||
storageTasks.get(0).run();
|
||||
verify(user, never()).getPlayer();
|
||||
platform.get().run();
|
||||
@@ -191,12 +193,12 @@ class CommandLoaderSchedulingTest {
|
||||
java.util.List<String> uuids = new java.util.ArrayList<>();
|
||||
for (int index = 0; index < 65; index++) uuids.add(UUID.randomUUID().toString());
|
||||
when(plugin.getUserManager().getAllUUIDs()).thenReturn(new java.util.ArrayList<>(uuids));
|
||||
when(plugin.getVotingPluginUserManager().getVotingPluginUser(any(UUID.class))).thenReturn(user);
|
||||
when(plugin.getVotingPluginUserManager().getVotingPluginUser(any(UUID.class), eq(true))).thenReturn(user);
|
||||
java.util.List<Runnable> platformTasks = new java.util.ArrayList<>();
|
||||
org.mockito.Mockito.doAnswer(call -> { platformTasks.add(call.getArgument(1)); return null; })
|
||||
.when(scheduler).runTask(eq(plugin), any(Runnable.class));
|
||||
|
||||
new CommandLoader(plugin).loadAllVotingUsersAsync(sender, ignored -> { });
|
||||
CommandLoaderAccess.loadAllVotingUsersAsync(new CommandLoader(plugin), sender, ignored -> { });
|
||||
storageTasks.get(0).run();
|
||||
org.junit.jupiter.api.Assertions.assertEquals(1, platformTasks.size());
|
||||
platformTasks.get(0).run();
|
||||
+494
@@ -0,0 +1,494 @@
|
||||
package com.bencodez.votingplugin.tests.commands;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
|
||||
import com.bencodez.votingplugin.commands.CommandLoader;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandMap;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.command.SimpleCommandMap;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.PluginDescriptionFile;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import com.bencodez.advancedcore.AdvancedCoreConfigOptions;
|
||||
import com.bencodez.advancedcore.api.command.CommandHandler;
|
||||
import io.github.projectunified.unidialog.core.payload.DialogPayload;
|
||||
import com.bencodez.simpleapi.dialog.UniDialogService;
|
||||
import com.bencodez.simpleapi.dialog.MultiActionDialogBuilder;
|
||||
import com.bencodez.simpleapi.scheduler.BukkitScheduler;
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
import com.bencodez.votingplugin.commands.gui.player.VoteHelp;
|
||||
import com.bencodez.votingplugin.commands.gui.player.VoteURL;
|
||||
import com.bencodez.votingplugin.config.Config;
|
||||
import com.bencodez.votingplugin.config.GUI;
|
||||
import com.bencodez.votingplugin.user.VotingPluginUser;
|
||||
import com.bencodez.votingplugin.votesites.VoteSiteManager;
|
||||
|
||||
class ConfigurableVoteCommandTest {
|
||||
@Test
|
||||
void absentConfigurationKeysLoadTheExistingDefaults() throws Exception {
|
||||
Config config = mock(Config.class);
|
||||
new com.bencodez.simpleapi.file.annotation.AnnotationHandler()
|
||||
.load(new org.bukkit.configuration.file.YamlConfiguration(), config);
|
||||
var enabled = Config.class.getDeclaredField("voteCommandEnabled");
|
||||
var name = Config.class.getDeclaredField("voteCommandName");
|
||||
enabled.setAccessible(true);
|
||||
name.setAccessible(true);
|
||||
assertTrue(enabled.getBoolean(config));
|
||||
assertEquals("vote", name.get(config));
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultRootKeepsDispatchPermissionsStandaloneAliasesAndTabCompletion() throws Exception {
|
||||
try (Fixture f = new Fixture(true, "vote", true)) {
|
||||
PluginCommand original = f.owned.get("vote");
|
||||
f.loader.loadAliases();
|
||||
assertSame(original, f.map.getCommand("vote"));
|
||||
assertSame(original, f.map.getCommand("votingplugin:vote"));
|
||||
assertSame(f.owned.get("votegui"), f.map.getCommand("vgui"));
|
||||
assertEquals("VotingPlugin.Commands.Vote.GUI", f.owned.get("votegui").getPermission());
|
||||
assertEquals(List.of("GUI", "Help"), original.tabComplete(f.player, "vote", new String[] { "" }));
|
||||
assertTrue(f.map.dispatch(f.player, "vote GUI"));
|
||||
assertEquals(1, f.executed.get());
|
||||
assertEquals("/vote", f.plugin.getVoteCommandPrefix());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void disableRemovesAllOwnedPlayerLabelsAndNamespacesButKeepsAdminAndForeignCommands() throws Exception {
|
||||
try (Fixture f = new Fixture(false, "vote", true)) {
|
||||
Command foreign = f.foreign("votechallenge", "other");
|
||||
f.loader.loadAliases();
|
||||
for (String name : List.of("vote", "votehelp", "vhelp", "votegui", "vgui")) {
|
||||
assertNull(f.map.getCommand(name), name);
|
||||
assertNull(f.map.getCommand("votingplugin:" + name), name);
|
||||
}
|
||||
assertSame(f.owned.get("adminvote"), f.map.getCommand("adminvote"));
|
||||
assertSame(f.owned.get("av"), f.map.getCommand("av"));
|
||||
assertSame(f.owned.get("adminvotehelp"), f.map.getCommand("avhelp"));
|
||||
assertNotNull(f.owned.get("adminvotehelp").getExecutor());
|
||||
assertSame(foreign, f.map.getCommand("votechallenge"));
|
||||
assertFalse(f.plugin.isVoteCommandEnabled());
|
||||
assertFalse(f.map.dispatch(f.player, "votingplugin:vote GUI"));
|
||||
f.loader.processSlotClick(f.player, mock(VotingPluginUser.class), "help");
|
||||
verify(f.player, never()).performCommand(anyString());
|
||||
assertTrue(new VoteHelp(f.plugin, f.player, 1).helpText(f.player).isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void renameRemovesOldRootAndRetainsCanonicalStandaloneAliases() throws Exception {
|
||||
try (Fixture f = new Fixture(true, " Ballot ", true)) {
|
||||
f.loader.loadAliases();
|
||||
assertNull(f.map.getCommand("vote"));
|
||||
assertNull(f.map.getCommand("votingplugin:vote"));
|
||||
assertSame(f.owned.get("vote"), f.map.getCommand("ballot"));
|
||||
assertSame(f.owned.get("vote"), f.map.getCommand("votingplugin:ballot"));
|
||||
assertSame(f.owned.get("votegui"), f.map.getCommand("vgui"));
|
||||
assertNull(f.map.getCommand("ballotgui"));
|
||||
assertEquals("/ballot", f.plugin.getVoteCommandPrefix());
|
||||
assertTrue(f.map.dispatch(f.player, "ballot GUI"));
|
||||
assertTrue(f.map.dispatch(f.player, "vgui"));
|
||||
assertEquals(2, f.executed.get());
|
||||
assertEquals(List.of("GUI", "Help"), f.owned.get("vote").tabComplete(f.player, "ballot", new String[] { "" }));
|
||||
assertEquals("VotingPlugin.Commands.Vote.GUI", f.owned.get("votegui").getPermission());
|
||||
assertTrue(f.loader.hasPermission(f.player, "ballot GUI"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void syntheticPlayerAndAdminAliasesCannotReplaceRenamedRoot() throws Exception {
|
||||
for (boolean aliases : List.of(true, false)) {
|
||||
for (String name : List.of("votetogglereminders", "adminvotesynthetic")) {
|
||||
try (Fixture f = new Fixture(true, name, aliases)) {
|
||||
f.plugin.getVoteCommand().add(f.handler("ToggleReminders", "VotingPlugin.Commands.Vote.ToggleReminders"));
|
||||
f.plugin.getAdminVoteCommand().add(f.handler("Synthetic", "VotingPlugin.Commands.AdminVote.Synthetic"));
|
||||
f.loader.loadAliases();
|
||||
PluginCommand root = f.owned.get("vote");
|
||||
assertSame(root, f.map.getCommand(name));
|
||||
assertInstanceOf(com.bencodez.votingplugin.commands.executers.CommandVote.class, root.getExecutor());
|
||||
assertFalse(f.loader.getAliasCommandNames().contains(name));
|
||||
assertTrue(f.map.dispatch(f.player, name + " GUI"));
|
||||
assertTrue(f.map.dispatch(f.player, name + " Help"));
|
||||
assertEquals(2, f.executed.get());
|
||||
assertTrue(root.tabComplete(f.player, name, new String[] { "" }).contains("GUI"));
|
||||
assertNotEquals("VotingPlugin.Commands.AdminVote.Synthetic", root.getPermission());
|
||||
assertNotEquals("VotingPlugin.Commands.Vote.ToggleReminders", root.getPermission());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void disabledSyntheticAliasCannotUnregisterRenamedRoot() throws Exception {
|
||||
try (Fixture f = new Fixture(true, "votetogglereminders", true)) {
|
||||
var field = CommandLoader.class.getDeclaredField("disabledAliasCommandNames");
|
||||
field.setAccessible(true);
|
||||
@SuppressWarnings("unchecked") var disabled = (java.util.Set<String>) field.get(f.loader);
|
||||
disabled.add("votetogglereminders");
|
||||
disabled.add("votegui");
|
||||
f.loader.loadAliases();
|
||||
assertSame(f.owned.get("vote"), f.map.getCommand("votetogglereminders"));
|
||||
assertSame(f.owned.get("vote"), f.map.getCommand("votingplugin:votetogglereminders"));
|
||||
assertTrue(f.loader.isVoteCommandEnabled());
|
||||
assertTrue(f.map.dispatch(f.player, "votetogglereminders Help"));
|
||||
assertEquals(1, f.executed.get());
|
||||
assertNull(f.map.getCommand("votegui"));
|
||||
assertNull(f.map.getCommand("vgui"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void foreignBareLabelKeepsItsOwnerAndGeneratedActionsUseTheVotingPluginNamespace() throws Exception {
|
||||
try (Fixture f = new Fixture(true, "ballot", true)) {
|
||||
Command foreign = f.foreign("ballot", "other");
|
||||
f.loader.loadAliases();
|
||||
assertSame(foreign, f.map.getCommand("ballot"));
|
||||
assertSame(f.owned.get("vote"), f.map.getCommand("votingplugin:ballot"));
|
||||
assertEquals("/votingplugin:ballot", f.plugin.getVoteCommandPrefix());
|
||||
f.loader.processSlotClick(f.player, mock(VotingPluginUser.class), "help");
|
||||
verify(f.player).performCommand("votingplugin:ballot help");
|
||||
assertFalse(f.loader.isVotingPluginCommand(f.player, "ballot"));
|
||||
assertTrue(f.loader.isVotingPluginCommand(f.player, "votingplugin:ballot"));
|
||||
assertTrue(f.map.dispatch(f.player, "votingplugin:ballot GUI"));
|
||||
assertEquals(1, f.executed.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void renamedFallbackPublishesThroughForwardingMapWithoutReplacingForeignCommand() throws Exception {
|
||||
try (Fixture f = new Fixture(true, "ballot", true)) {
|
||||
f.map.useForwardingStorage();
|
||||
Command foreign = f.foreign("ballot", "other");
|
||||
f.loader.loadAliases();
|
||||
assertSame(foreign, f.map.getCommand("ballot"));
|
||||
assertSame(f.owned.get("vote"), f.map.getCommand("votingplugin:ballot"));
|
||||
assertTrue(f.plugin.isVoteCommandEnabled());
|
||||
assertEquals("/votingplugin:ballot", f.plugin.getVoteCommandPrefix());
|
||||
assertTrue(f.map.dispatch(f.player, "votingplugin:ballot GUI"));
|
||||
assertEquals(1, f.executed.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void occupiedNamespaceCannotBeOverwrittenEvenWhenBareLabelIsFree() throws Exception {
|
||||
try (Fixture f = new Fixture(true, "ballot", true)) {
|
||||
Command foreign = f.foreign("ballot", "other");
|
||||
f.map.known().remove("ballot");
|
||||
f.map.known().put("votingplugin:ballot", foreign);
|
||||
f.loader.loadAliases();
|
||||
assertSame(foreign, f.map.getCommand("votingplugin:ballot"));
|
||||
assertNull(f.map.getCommand("ballot"));
|
||||
assertSame(f.owned.get("vote"), f.map.getCommand("vote"));
|
||||
assertEquals("/vote", f.plugin.getVoteCommandPrefix());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void foreignBareAliasCannotBeReplacedByBukkitsPrimaryCommandRegistration() throws Exception {
|
||||
try (Fixture f = new Fixture(true, "ballot", true)) {
|
||||
Command foreign = f.foreign("poll", "other");
|
||||
f.map.known().put("ballot", foreign);
|
||||
f.loader.loadAliases();
|
||||
assertSame(foreign, f.map.getCommand("ballot"));
|
||||
assertSame(foreign, f.map.getCommand("poll"));
|
||||
assertSame(f.owned.get("vote"), f.map.getCommand("votingplugin:ballot"));
|
||||
assertEquals("/votingplugin:ballot", f.plugin.getVoteCommandPrefix());
|
||||
assertTrue(f.map.dispatch(f.player, "votingplugin:ballot GUI"));
|
||||
assertEquals(1, f.executed.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidNamesAndOwnedAdminOrPlayerAliasNamesFallBackWithoutChangingThoseCommands() throws Exception {
|
||||
for (String name : List.of("", "/ballot", "ballot GUI", "other:ballot", "bad\nname", "av", "adminvote", "avhelp", "fakevote", "votegui", "vgui")) {
|
||||
try (Fixture f = new Fixture(true, name, true)) {
|
||||
f.loader.loadAliases();
|
||||
assertEquals("/vote", f.plugin.getVoteCommandPrefix(), name);
|
||||
assertSame(f.owned.get("adminvote"), f.map.getCommand("adminvote"));
|
||||
assertSame(f.owned.get("av"), f.map.getCommand("av"));
|
||||
assertSame(f.owned.get("adminvotehelp"), f.map.getCommand("avhelp"));
|
||||
assertSame(f.owned.get("votegui"), f.map.getCommand("vgui"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void disablingRestoresOtherPluginsFallbackAndRemovesDisplacedOwnedAliasObjects() throws Exception {
|
||||
try (Fixture f = new Fixture(false, "vote", true)) {
|
||||
Command foreignVote = f.foreign("vote", "other");
|
||||
Command foreignGui = f.foreign("votegui", "other");
|
||||
// Simulate both canonical labels being replaced, leaving only vgui.
|
||||
f.map.known().put("votegui", foreignGui);
|
||||
f.map.known().put("votingplugin:votegui", foreignGui);
|
||||
f.loader.loadAliases();
|
||||
assertSame(foreignVote, f.map.getCommand("vote"));
|
||||
assertSame(foreignGui, f.map.getCommand("votegui"));
|
||||
assertSame(foreignGui, f.map.getCommand("votingplugin:votegui"));
|
||||
assertNull(f.map.getCommand("vgui"));
|
||||
assertNull(f.map.getCommand("votingplugin:vgui"));
|
||||
assertNull(f.map.getCommand("votingplugin:vote"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void aliasToggleRemovesPlayerAndAdminStandaloneAliasesButPreservesRenamedRootAndAdminRoots() throws Exception {
|
||||
try (Fixture f = new Fixture(true, "ballot", false)) {
|
||||
Command foreign = f.foreign("votegui", "other");
|
||||
f.loader.loadAliases();
|
||||
assertSame(foreign, f.map.getCommand("votegui"));
|
||||
assertNull(f.map.getCommand("votingplugin:votegui"));
|
||||
assertNull(f.map.getCommand("vgui"));
|
||||
assertNull(f.map.getCommand("adminvotehelp"));
|
||||
assertNull(f.map.getCommand("avhelp"));
|
||||
assertSame(f.owned.get("vote"), f.map.getCommand("ballot"));
|
||||
assertSame(f.owned.get("adminvote"), f.map.getCommand("adminvote"));
|
||||
assertSame(f.owned.get("av"), f.map.getCommand("av"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void reloadCannotChangeCapturedRootToggleAliasesHelpOrNavigation() throws Exception {
|
||||
try (Fixture f = new Fixture(true, "ballot", true)) {
|
||||
f.loader.loadAliases();
|
||||
when(f.config.isVoteCommandEnabled()).thenReturn(false);
|
||||
when(f.config.getVoteCommandName()).thenReturn("newroot");
|
||||
when(f.config.isLoadCommandAliases()).thenReturn(false);
|
||||
f.loader.loadAliases();
|
||||
assertTrue(f.plugin.isVoteCommandEnabled());
|
||||
assertEquals("/ballot", f.plugin.getVoteCommandPrefix());
|
||||
assertSame(f.owned.get("vote"), f.map.getCommand("ballot"));
|
||||
assertNull(f.map.getCommand("newroot"));
|
||||
assertSame(f.owned.get("votegui"), f.map.getCommand("vgui"));
|
||||
f.loader.processSlotClick(f.player, mock(VotingPluginUser.class), "help");
|
||||
verify(f.player).performCommand("ballot help");
|
||||
VoteHelp help = new VoteHelp(f.plugin, f.player, 1);
|
||||
assertTrue(help.helpText(f.player).stream().allMatch(line -> line.getClickEvent().getValue().startsWith("/ballot ")));
|
||||
assertEquals(List.of("GUI", "Help"), f.owned.get("vote").tabComplete(f.player, "ballot", new String[] { "" }));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void disabledStartupCannotBecomeRegisteredThroughReload() throws Exception {
|
||||
try (Fixture f = new Fixture(false, "vote", true)) {
|
||||
f.loader.loadAliases();
|
||||
when(f.config.isVoteCommandEnabled()).thenReturn(true);
|
||||
when(f.config.getVoteCommandName()).thenReturn("ballot");
|
||||
f.loader.loadAliases();
|
||||
assertFalse(f.plugin.isVoteCommandEnabled());
|
||||
assertNull(f.map.getCommand("vote"));
|
||||
assertNull(f.map.getCommand("ballot"));
|
||||
assertNull(f.map.getCommand("vgui"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void clickableAndLegacyHelpAndExistingInvalidHintUseActiveRoot() throws Exception {
|
||||
try (Fixture f = new Fixture(true, "ballot", true)) {
|
||||
f.loader.loadAliases();
|
||||
VoteHelp help = new VoteHelp(f.plugin, f.player, 1);
|
||||
assertEquals(2, help.helpText(f.player).size());
|
||||
assertEquals(2, help.helpTextLegacy(f.player).size());
|
||||
assertTrue(help.helpText(f.player).stream().allMatch(line -> line.getClickEvent().getValue().startsWith("/ballot ")));
|
||||
assertTrue(help.helpTextLegacy(f.player).stream().allMatch(line -> line.getClickEvent().getValue().startsWith("/ballot ")));
|
||||
assertEquals("see /ballot help!", f.plugin.formatVoteCommandMessage("see /vote help!"));
|
||||
assertEquals("see /ballot help!", f.plugin.formatVoteCommandMessage("see %votecommand% help!"));
|
||||
assertEquals("/votechallenge /adminvote", f.plugin.formatVoteCommandMessage("/votechallenge /adminvote"));
|
||||
f.map.dispatch(f.player, "ballot unknown");
|
||||
verify(f.player).sendMessage("see /ballot help!");
|
||||
f.map.dispatch(f.player, "vgui unknown");
|
||||
verify(f.player, times(2)).sendMessage("see /ballot help!");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void urlDialogBackButtonUsesCapturedRootAndDoesNothingWhenPlayerCommandsAreDisabled() throws Exception {
|
||||
for (boolean enabled : List.of(true, false)) {
|
||||
try (Fixture f = new Fixture(enabled, "ballot", true)) {
|
||||
f.loader.loadAliases();
|
||||
UniDialogService service = mock(UniDialogService.class);
|
||||
MultiActionDialogBuilder dialog = mock(MultiActionDialogBuilder.class, RETURNS_SELF);
|
||||
GUI gui = mock(GUI.class);
|
||||
VoteSiteManager sites = mock(VoteSiteManager.class);
|
||||
when(f.plugin.getDialogService()).thenReturn(service);
|
||||
when(service.multiAction(f.player)).thenReturn(dialog);
|
||||
when(f.plugin.getGui()).thenReturn(gui);
|
||||
when(f.plugin.getVoteSiteManager()).thenReturn(sites);
|
||||
when(sites.getVoteSitesEnabled()).thenReturn(new ArrayList<>());
|
||||
when(gui.isChestVoteURLBackButton()).thenReturn(true);
|
||||
new VoteURL(f.plugin, f.player, mock(VotingPluginUser.class), true).onDialog(f.player);
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Consumer<DialogPayload>> callback = ArgumentCaptor.forClass(Consumer.class);
|
||||
verify(dialog).button(eq("&eBack"), eq("&7Return to previous menu"), callback.capture());
|
||||
callback.getValue().accept(null);
|
||||
if (enabled) verify(f.player).performCommand("ballot gui");
|
||||
else verify(f.player, never()).performCommand(anyString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Exposes the same public method provided by CraftServer, without CraftBukkit packages. */
|
||||
public interface CommandMapAccess { CommandMap getCommandMap(); }
|
||||
|
||||
private static final class TestCommandMap extends SimpleCommandMap {
|
||||
TestCommandMap(Server server) { super(server); }
|
||||
Map<String, Command> known() { return knownCommands; }
|
||||
void useForwardingStorage() throws Exception {
|
||||
var forwarding = new ForwardingStorage();
|
||||
forwarding.putAll(knownCommands);
|
||||
var field = SimpleCommandMap.class.getDeclaredField("knownCommands");
|
||||
field.setAccessible(true);
|
||||
field.set(this, forwarding);
|
||||
}
|
||||
}
|
||||
|
||||
// Paper's bridge overrides the basic map operations but inherits HashMap's
|
||||
// putIfAbsent, which writes backing storage instead of the command dispatcher.
|
||||
private static final class ForwardingStorage extends HashMap<String, Command> {
|
||||
private final Map<String, Command> dispatcher = new java.util.LinkedHashMap<>();
|
||||
@Override public Command get(Object key) { return dispatcher.get(key); }
|
||||
@Override public Command put(String key, Command value) { return dispatcher.put(key, value); }
|
||||
@Override public void putAll(Map<? extends String, ? extends Command> values) { values.forEach(this::put); }
|
||||
@Override public Command remove(Object key) { return dispatcher.remove(key); }
|
||||
@Override public boolean remove(Object key, Object value) { return dispatcher.remove(key, value); }
|
||||
@Override public boolean containsKey(Object key) { return dispatcher.containsKey(key); }
|
||||
@Override public boolean containsValue(Object value) { return dispatcher.containsValue(value); }
|
||||
@Override public int size() { return dispatcher.size(); }
|
||||
@Override public boolean isEmpty() { return dispatcher.isEmpty(); }
|
||||
@Override public void clear() { dispatcher.clear(); }
|
||||
@Override public java.util.Set<Map.Entry<String, Command>> entrySet() { return dispatcher.entrySet(); }
|
||||
@Override public java.util.Set<String> keySet() { return dispatcher.keySet(); }
|
||||
@Override public Collection<Command> values() { return dispatcher.values(); }
|
||||
}
|
||||
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
final VotingPluginMain previousPlugin = VotingPluginMain.plugin;
|
||||
final VotingPluginMain plugin = mock(VotingPluginMain.class);
|
||||
final Config config = mock(Config.class);
|
||||
final Player player = mock(Player.class);
|
||||
final MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
final TestCommandMap map;
|
||||
final Map<String, PluginCommand> owned = new HashMap<>();
|
||||
final CommandLoader loader;
|
||||
final AtomicInteger executed = new AtomicInteger();
|
||||
|
||||
Fixture(boolean enabled, String name, boolean aliases) throws Exception {
|
||||
VotingPluginMain.plugin = plugin;
|
||||
try {
|
||||
Server server = mock(Server.class, withSettings().extraInterfaces(CommandMapAccess.class));
|
||||
PluginManager manager = mock(PluginManager.class);
|
||||
Logger logger = Logger.getLogger("ConfigurableVoteCommandTest");
|
||||
bukkit.when(Bukkit::getServer).thenReturn(server);
|
||||
bukkit.when(Bukkit::getPluginManager).thenReturn(manager);
|
||||
bukkit.when(Bukkit::getLogger).thenReturn(logger);
|
||||
bukkit.when(Bukkit::getVersion).thenReturn("Mock (MC: 1.21.9)");
|
||||
bukkit.when(Bukkit::getBukkitVersion).thenReturn("1.21.9-R0.1-SNAPSHOT");
|
||||
map = new TestCommandMap(server);
|
||||
when(((CommandMapAccess) server).getCommandMap()).thenReturn(map);
|
||||
when(plugin.getServer()).thenReturn(server);
|
||||
when(plugin.getConfigFile()).thenReturn(config);
|
||||
when(plugin.getLogger()).thenReturn(logger);
|
||||
when(plugin.isEnabled()).thenReturn(true);
|
||||
when(plugin.getName()).thenReturn("VotingPlugin");
|
||||
PluginDescriptionFile description = new PluginDescriptionFile(getClass().getResourceAsStream("/plugin.yml"));
|
||||
when(plugin.getDescription()).thenReturn(description);
|
||||
AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class);
|
||||
when(plugin.getOptions()).thenReturn(options);
|
||||
when(options.isMultiplePermissionChecks()).thenReturn(true);
|
||||
when(config.isVoteCommandEnabled()).thenReturn(enabled);
|
||||
when(config.getVoteCommandName()).thenReturn(name);
|
||||
when(config.isLoadCommandAliases()).thenReturn(aliases);
|
||||
when(config.getFormatCommandsVoteHelpHoverColor()).thenReturn("aqua");
|
||||
when(config.getFormatCommandsVoteHelpLine()).thenReturn("%Command% : %HelpMessage%");
|
||||
when(config.getFormatInvalidCommandVote()).thenReturn("see /vote help!");
|
||||
BukkitScheduler scheduler = mock(BukkitScheduler.class);
|
||||
when(plugin.getBukkitScheduler()).thenReturn(scheduler);
|
||||
doAnswer(call -> { ((Runnable) call.getArgument(1)).run(); return null; })
|
||||
.when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
|
||||
ArrayList<CommandHandler> playerHandlers = new ArrayList<>();
|
||||
playerHandlers.add(handler("GUI", "VotingPlugin.Commands.Vote.GUI"));
|
||||
playerHandlers.add(handler("Help", "VotingPlugin.Commands.Vote.Help"));
|
||||
when(plugin.getVoteCommand()).thenReturn(playerHandlers);
|
||||
var adminHandlers = new ArrayList<>(List.of(handler("Help", "VotingPlugin.Commands.AdminVote.Help")));
|
||||
when(plugin.getAdminVoteCommand()).thenReturn(adminHandlers);
|
||||
when(player.hasPermission(anyString())).thenReturn(true);
|
||||
for (var entry : description.getCommands().entrySet()) {
|
||||
PluginCommand command = command(entry.getKey(), plugin);
|
||||
Object declaredAliases = entry.getValue().get("aliases");
|
||||
if (declaredAliases instanceof String alias) command.setAliases(List.of(alias));
|
||||
else if (declaredAliases instanceof Collection<?> values)
|
||||
command.setAliases(values.stream().map(String::valueOf).toList());
|
||||
owned.put(entry.getKey(), command);
|
||||
map.register("votingplugin", command);
|
||||
}
|
||||
when(plugin.getCommand(anyString())).thenAnswer(call -> {
|
||||
String key = ((String) call.getArgument(0)).toLowerCase(java.util.Locale.ROOT);
|
||||
Command command = map.getCommand(key);
|
||||
if (!(command instanceof PluginCommand result) || result.getPlugin() != plugin)
|
||||
command = map.getCommand("votingplugin:" + key);
|
||||
return command instanceof PluginCommand result && result.getPlugin() == plugin ? result : null;
|
||||
});
|
||||
loader = new CommandLoader(plugin);
|
||||
var field = VotingPluginMain.class.getDeclaredField("commandLoader");
|
||||
field.setAccessible(true);
|
||||
field.set(plugin, loader);
|
||||
when(plugin.getCommandLoader()).thenReturn(loader);
|
||||
when(plugin.getVoteCommandPrefix()).thenCallRealMethod();
|
||||
when(plugin.isVoteCommandEnabled()).thenCallRealMethod();
|
||||
when(plugin.formatVoteCommandMessage(anyString())).thenCallRealMethod();
|
||||
} catch (Exception | Error failure) {
|
||||
bukkit.close();
|
||||
VotingPluginMain.plugin = previousPlugin;
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
CommandHandler handler(String argument, String permission) {
|
||||
return new CommandHandler(plugin, new String[] { argument }, permission, "Open menu") {
|
||||
@Override public void execute(CommandSender sender, String[] args) { executed.incrementAndGet(); }
|
||||
};
|
||||
}
|
||||
|
||||
Command foreign(String label, String namespace) throws Exception {
|
||||
Plugin owner = mock(Plugin.class);
|
||||
when(owner.getName()).thenReturn("OtherPlugin");
|
||||
PluginCommand command = command(label, owner);
|
||||
map.register(namespace, command);
|
||||
return command;
|
||||
}
|
||||
|
||||
static PluginCommand command(String name, Plugin owner) throws Exception {
|
||||
Constructor<PluginCommand> constructor = PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class);
|
||||
constructor.setAccessible(true);
|
||||
return ReflectiveAccess.newInstance(constructor, new Object[]{name, owner});
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
bukkit.close();
|
||||
VotingPluginMain.plugin = previousPlugin;
|
||||
}
|
||||
}
|
||||
}
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
package com.bencodez.votingplugin.tests.commands;
|
||||
|
||||
import com.bencodez.votingplugin.commands.CommandLoader;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.bencodez.advancedcore.api.command.CommandHandler;
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
import com.bencodez.votingplugin.config.Config;
|
||||
|
||||
class GuidedSessionCommandRegistrationTest {
|
||||
@Test void disabledGuidePreservesTheExistingCustomSessionHandler() throws Exception {
|
||||
Fixture f = new Fixture();
|
||||
f.register();
|
||||
assertEquals(1, f.sessionHandlers().size());
|
||||
assertEquals("existing owner command", f.sessionHandlers().get(0).getHelpMessage());
|
||||
assertFalse(f.plugin.getVoteCommand().stream().anyMatch(h -> h.getArgs().length == 2
|
||||
&& h.getArgs()[0].equalsIgnoreCase("Session")));
|
||||
}
|
||||
|
||||
@Test void enabledGuideReservesSessionAndAllNavigationHandlers() throws Exception {
|
||||
Fixture f = new Fixture();
|
||||
f.data.set("GuidedVotingSession.Enabled", true);
|
||||
f.register();
|
||||
assertEquals(8, f.sessionHandlers().size()); // seven built-ins plus the owner custom command
|
||||
assertEquals("Guided voting session", f.sessionHandlers().get(0).getHelpMessage());
|
||||
for (String action : List.of("next", "previous", "skip", "check", "finish", "restart")) {
|
||||
assertTrue(f.sessionHandlers().stream().anyMatch(h -> h.getArgs().length == 2
|
||||
&& h.getArgs()[1].equals(action)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void disablingAndReloadingRestoresCustomRoutingWithoutDuplicateHandlers() throws Exception {
|
||||
Fixture f = new Fixture();
|
||||
f.data.set("GuidedVotingSession.Enabled", true);
|
||||
f.register();
|
||||
f.data.set("GuidedVotingSession.Enabled", false);
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
assertEquals(1, f.sessionHandlers().size());
|
||||
assertEquals("existing owner command", f.sessionHandlers().get(0).getHelpMessage());
|
||||
}
|
||||
|
||||
@Test void enablingOnReloadAddsOnlyGuideHandlersAndPreservesProductionIdentity() throws Exception {
|
||||
Fixture f = new Fixture();
|
||||
f.register();
|
||||
var production = new ArrayList<>(f.plugin.getVoteCommand());
|
||||
f.data.set("GuidedVotingSession.Enabled", true);
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
assertEquals(production.size() + 7, f.plugin.getVoteCommand().size());
|
||||
for (CommandHandler original : production) assertTrue(f.plugin.getVoteCommand().contains(original));
|
||||
assertEquals("Guided voting session", f.sessionHandlers().get(0).getHelpMessage());
|
||||
}
|
||||
|
||||
@Test void reloadHonorsDisabledGuidePermissionWithoutRemovingCustomHandler() throws Exception {
|
||||
Fixture f = new Fixture(); f.register();
|
||||
f.data.set("GuidedVotingSession.Enabled", true);
|
||||
when(f.config.getDisabledCommands()).thenReturn(new ArrayList<>(List.of("VotingPlugin.Commands.Vote.Session")));
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
assertEquals(1, f.sessionHandlers().size());
|
||||
assertEquals("existing owner command", f.sessionHandlers().get(0).getHelpMessage());
|
||||
}
|
||||
|
||||
@Test void permissionLookupTracksEnableDisableAndRetainsUnrelatedCommands() throws Exception {
|
||||
Fixture f = new Fixture(); f.register();
|
||||
org.bukkit.entity.Player owner = mock(org.bukkit.entity.Player.class);
|
||||
when(owner.hasPermission("owner.session")).thenReturn(true);
|
||||
org.bukkit.entity.Player voter = mock(org.bukkit.entity.Player.class);
|
||||
when(voter.hasPermission("VotingPlugin.Commands.Vote.Session")).thenReturn(true);
|
||||
var index = new java.util.HashMap<String, CommandHandler>();
|
||||
CommandHandler custom = f.sessionHandlers().get(0);
|
||||
CommandHandler unrelated = f.plugin.getVoteCommand().get(0);
|
||||
index.put("voteSession", custom);
|
||||
index.put("voteHelp", unrelated);
|
||||
f.loader.setCommands(index);
|
||||
assertTrue(f.loader.hasPermission(owner, "voteSession"));
|
||||
assertFalse(f.loader.hasPermission(voter, "voteSession"));
|
||||
f.data.set("GuidedVotingSession.Enabled", true);
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
assertFalse(f.loader.hasPermission(owner, "voteSession"));
|
||||
assertTrue(f.loader.hasPermission(voter, "votingplugin:voteSession"));
|
||||
assertSame(unrelated, index.get("voteHelp"));
|
||||
f.data.set("GuidedVotingSession.Enabled", false);
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
assertSame(custom, index.get("voteSession"));
|
||||
assertTrue(f.loader.hasPermission(owner, "voteSession"));
|
||||
assertFalse(f.loader.hasPermission(voter, "voteSession"));
|
||||
}
|
||||
|
||||
@Test void initialAliasIndexUsesGuideRatherThanCustomOrNavigationHandler() throws Exception {
|
||||
assertInitialAliasIndexUsesGuide("vote");
|
||||
}
|
||||
|
||||
@Test void renamedPlayerRootKeepsTheGuidedSessionPermissionIndex() throws Exception {
|
||||
assertInitialAliasIndexUsesGuide("ballot");
|
||||
}
|
||||
|
||||
public interface CommandMapAccess { org.bukkit.command.CommandMap getCommandMap(); }
|
||||
|
||||
private void assertInitialAliasIndexUsesGuide(String commandName) throws Exception {
|
||||
Fixture f = new Fixture(commandName);
|
||||
f.data.set("GuidedVotingSession.Enabled", true);
|
||||
f.register();
|
||||
when(f.plugin.getAdminVoteCommand()).thenReturn(new ArrayList<>());
|
||||
when(f.plugin.getDescription().getCommands()).thenReturn(java.util.Map.of());
|
||||
when(f.plugin.getDescription().getName()).thenReturn("VotingPlugin");
|
||||
when(f.plugin.getName()).thenReturn("VotingPlugin");
|
||||
org.bukkit.Server server = mock(org.bukkit.Server.class, withSettings().extraInterfaces(CommandMapAccess.class));
|
||||
var map = new org.bukkit.command.SimpleCommandMap(server);
|
||||
when(((CommandMapAccess) server).getCommandMap()).thenReturn(map);
|
||||
when(f.plugin.getServer()).thenReturn(server);
|
||||
var constructor = org.bukkit.command.PluginCommand.class.getDeclaredConstructor(String.class, org.bukkit.plugin.Plugin.class);
|
||||
constructor.setAccessible(true);
|
||||
var root = constructor.newInstance("vote", f.plugin);
|
||||
map.register("votingplugin", root);
|
||||
when(f.plugin.getCommand("vote")).thenReturn(root);
|
||||
try (var bukkit = mockStatic(org.bukkit.Bukkit.class)) {
|
||||
f.loader.loadAliases();
|
||||
}
|
||||
assertSame(root, map.getCommand(commandName));
|
||||
assertTrue(f.loader.isVoteCommandEnabled());
|
||||
CommandHandler indexed = f.loader.getCommands().get("voteSession");
|
||||
assertEquals("Guided voting session", indexed.getHelpMessage());
|
||||
assertArrayEquals(new String[] {"Session"}, indexed.getArgs());
|
||||
org.bukkit.entity.Player voter = mock(org.bukkit.entity.Player.class);
|
||||
when(voter.hasPermission("VotingPlugin.Commands.Vote.Session")).thenReturn(true);
|
||||
assertTrue(f.loader.hasPermission(voter, "voteSession"));
|
||||
}
|
||||
|
||||
@Test void enablingOnReloadHonorsDisabledDefaultPermission() throws Exception {
|
||||
Fixture f = new Fixture(); f.register();
|
||||
when(f.plugin.getOptions().isMultiplePermissionChecks()).thenReturn(true);
|
||||
when(f.config.getDisabledDefaultPermissions()).thenReturn(new ArrayList<>(List.of("VotingPlugin.Commands.Vote.Session")));
|
||||
f.loader.setCommands(new java.util.HashMap<>());
|
||||
f.data.set("GuidedVotingSession.Enabled", true);
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
org.bukkit.entity.Player player = mock(org.bukkit.entity.Player.class);
|
||||
when(player.hasPermission("VotingPlugin.Player")).thenReturn(true);
|
||||
assertFalse(f.loader.hasPermission(player, "voteSession"));
|
||||
assertTrue(f.sessionHandlers().stream().filter(h -> h.getHelpMessage().equals("Guided voting session"))
|
||||
.allMatch(h -> h.getPerm().equals("VotingPlugin.Commands.Vote.Session")));
|
||||
when(player.hasPermission("VotingPlugin.Commands.Vote.Session")).thenReturn(true);
|
||||
assertTrue(f.loader.hasPermission(player, "voteSession"));
|
||||
}
|
||||
|
||||
@Test void guidedSessionUseRequiresTheRegisteredRootHandlerPolicy() throws Exception {
|
||||
Fixture f = new Fixture();
|
||||
when(f.plugin.getOptions().isMultiplePermissionChecks()).thenReturn(true);
|
||||
f.data.set("GuidedVotingSession.Enabled", true);
|
||||
f.register();
|
||||
var player = mock(org.bukkit.entity.Player.class);
|
||||
when(player.hasPermission("VotingPlugin.Player")).thenReturn(true);
|
||||
assertTrue(f.loader.canUseGuidedSession(player));
|
||||
|
||||
when(f.config.getDisabledDefaultPermissions()).thenReturn(new ArrayList<>(List.of("VotingPlugin.Commands.Vote.Session")));
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
assertFalse(f.loader.canUseGuidedSession(player));
|
||||
when(player.hasPermission("VotingPlugin.Commands.Vote.Session")).thenReturn(true);
|
||||
assertTrue(f.loader.canUseGuidedSession(player));
|
||||
|
||||
when(f.config.getDisabledCommands()).thenReturn(new ArrayList<>(List.of("VotingPlugin.Commands.Vote.Session")));
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
assertFalse(f.loader.canUseGuidedSession(player));
|
||||
}
|
||||
|
||||
@Test void guidedSessionUseRejectsMultiplePermissionFallbackAndCustomOnlyOrDisabledGuide() throws Exception {
|
||||
Fixture f = new Fixture();
|
||||
f.data.set("GuidedVotingSession.Enabled", true);
|
||||
f.register();
|
||||
var player = mock(org.bukkit.entity.Player.class);
|
||||
when(player.hasPermission("VotingPlugin.Player")).thenReturn(true);
|
||||
when(f.plugin.getOptions().isMultiplePermissionChecks()).thenReturn(false);
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
assertFalse(f.loader.canUseGuidedSession(player));
|
||||
when(player.hasPermission("VotingPlugin.Commands.Vote.Session")).thenReturn(true);
|
||||
assertTrue(f.loader.canUseGuidedSession(player));
|
||||
|
||||
f.data.set("GuidedVotingSession.Enabled", false);
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
assertFalse(f.loader.canUseGuidedSession(player));
|
||||
f.data.set("GuidedVotingSession.Enabled", true);
|
||||
when(f.config.getDisabledCommands()).thenReturn(new ArrayList<>(List.of("VotingPlugin.Commands.Vote.Session")));
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
assertFalse(f.loader.canUseGuidedSession(player));
|
||||
}
|
||||
|
||||
@Test void reloadReconcilesOnlyGuidePermissionInheritanceAndInvalidatesStaleTasks() throws Exception {
|
||||
Fixture f = new Fixture(); f.register();
|
||||
java.util.List<Runnable> tasks = new ArrayList<>();
|
||||
var scheduler = f.plugin.getBukkitScheduler();
|
||||
doAnswer(call -> { tasks.add(call.getArgument(1)); return null; })
|
||||
.when(scheduler).runTask(eq(f.plugin), any(Runnable.class));
|
||||
var manager = mock(org.bukkit.plugin.PluginManager.class);
|
||||
try (var bukkit = mockStatic(org.bukkit.Bukkit.class)) {
|
||||
bukkit.when(org.bukkit.Bukkit::getPluginManager).thenReturn(manager);
|
||||
var server = mock(org.bukkit.Server.class);
|
||||
when(server.getPluginManager()).thenReturn(manager);
|
||||
bukkit.when(org.bukkit.Bukkit::getServer).thenReturn(server);
|
||||
var parent = new org.bukkit.permissions.Permission("VotingPlugin.Player");
|
||||
parent.getChildren().put("owner.unrelated", true);
|
||||
when(manager.getPermission("VotingPlugin.Player")).thenReturn(parent);
|
||||
f.data.set("GuidedVotingSession.Enabled", true);
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
tasks.remove(0).run();
|
||||
assertEquals(true, parent.getChildren().get("VotingPlugin.Commands.Vote.Session"));
|
||||
when(f.config.getDisabledDefaultPermissions()).thenReturn(new ArrayList<>(List.of("VotingPlugin.Commands.Vote.Session")));
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
tasks.remove(0).run();
|
||||
assertFalse(parent.getChildren().containsKey("VotingPlugin.Commands.Vote.Session"));
|
||||
when(f.config.getDisabledDefaultPermissions()).thenReturn(new ArrayList<>());
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
tasks.remove(0).run();
|
||||
assertEquals(true, parent.getChildren().get("VotingPlugin.Commands.Vote.Session"));
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
Runnable staleEnable = tasks.remove(0);
|
||||
f.data.set("GuidedVotingSession.Enabled", false);
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
Runnable staleDisable = tasks.remove(0);
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
tasks.remove(0).run();
|
||||
staleEnable.run();
|
||||
staleDisable.run();
|
||||
assertFalse(parent.getChildren().containsKey("VotingPlugin.Commands.Vote.Session"));
|
||||
assertEquals(true, parent.getChildren().get("owner.unrelated"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void disablingGuideRetainsCustomSessionInheritanceUnlessOwnerDisablesDefaultPermission() throws Exception {
|
||||
Fixture f = new Fixture();
|
||||
f.data.set("CustomCommands.owner-session.Permission", "VotingPlugin.Commands.Vote.Session|VotingPlugin.Player");
|
||||
f.register();
|
||||
java.util.List<Runnable> tasks = new ArrayList<>();
|
||||
var scheduler = f.plugin.getBukkitScheduler();
|
||||
doAnswer(call -> { tasks.add(call.getArgument(1)); return null; })
|
||||
.when(scheduler).runTask(eq(f.plugin), any(Runnable.class));
|
||||
var manager = mock(org.bukkit.plugin.PluginManager.class);
|
||||
try (var bukkit = mockStatic(org.bukkit.Bukkit.class)) {
|
||||
var server = mock(org.bukkit.Server.class);
|
||||
when(server.getPluginManager()).thenReturn(manager);
|
||||
bukkit.when(org.bukkit.Bukkit::getServer).thenReturn(server);
|
||||
bukkit.when(org.bukkit.Bukkit::getPluginManager).thenReturn(manager);
|
||||
var parent = new org.bukkit.permissions.Permission("VotingPlugin.Player");
|
||||
when(manager.getPermission("VotingPlugin.Player")).thenReturn(parent);
|
||||
parent.getChildren().put("VotingPlugin.Commands.Vote.Session", true);
|
||||
f.data.set("GuidedVotingSession.Enabled", true);
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
tasks.remove(0).run();
|
||||
f.data.set("GuidedVotingSession.Enabled", false);
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
tasks.remove(0).run();
|
||||
assertEquals(true, parent.getChildren().get("VotingPlugin.Commands.Vote.Session"));
|
||||
assertEquals("existing owner command", f.sessionHandlers().get(0).getHelpMessage());
|
||||
when(f.config.getDisabledDefaultPermissions()).thenReturn(new ArrayList<>(List.of("VotingPlugin.Commands.Vote.Session")));
|
||||
f.loader.refreshGuidedSessionCommands();
|
||||
tasks.remove(0).run();
|
||||
assertFalse(parent.getChildren().containsKey("VotingPlugin.Commands.Vote.Session"));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
final VotingPluginMain plugin = mock(VotingPluginMain.class, RETURNS_DEEP_STUBS);
|
||||
final Config config = mock(Config.class);
|
||||
final CommandLoader loader;
|
||||
final YamlConfiguration data = new YamlConfiguration();
|
||||
final AtomicReference<ArrayList<CommandHandler>> handlers = new AtomicReference<>();
|
||||
Fixture() { this("vote"); }
|
||||
Fixture(String commandName) {
|
||||
when(plugin.getConfigFile()).thenReturn(config);
|
||||
when(config.isVoteCommandEnabled()).thenReturn(true);
|
||||
when(config.getVoteCommandName()).thenReturn(commandName);
|
||||
loader = new CommandLoader(plugin);
|
||||
when(config.getData()).thenReturn(data);
|
||||
when(config.getDisabledCommands()).thenReturn(new ArrayList<>());
|
||||
when(config.isAddCustomCommands()).thenReturn(true);
|
||||
when(config.getCustomCommands()).thenReturn(Set.of("owner-session"));
|
||||
var custom = data.createSection("CustomCommands.owner-session");
|
||||
custom.set("Args", new ArrayList<>(List.of("Session")));
|
||||
custom.set("HelpMessage", "existing owner command");
|
||||
custom.set("Permission", "owner.session");
|
||||
when(config.getCustomCommands("owner-session")).thenReturn(custom);
|
||||
doAnswer(call -> { handlers.set(call.getArgument(0)); return null; })
|
||||
.when(plugin).setVoteCommand(any(ArrayList.class));
|
||||
when(plugin.getVoteCommand()).thenAnswer(call -> handlers.get());
|
||||
}
|
||||
void register() throws Exception {
|
||||
Method load = CommandLoader.class.getDeclaredMethod("loadVoteCommand");
|
||||
load.setAccessible(true);
|
||||
try (var singleton = mockStatic(com.bencodez.advancedcore.command.CommandLoader.class)) {
|
||||
var core = mock(com.bencodez.advancedcore.command.CommandLoader.class);
|
||||
singleton.when(com.bencodez.advancedcore.command.CommandLoader::getInstance).thenReturn(core);
|
||||
when(core.getBasicCommands("VotingPlugin")).thenReturn(new ArrayList<>());
|
||||
load.invoke(loader);
|
||||
}
|
||||
}
|
||||
List<CommandHandler> sessionHandlers() {
|
||||
return plugin.getVoteCommand().stream().filter(h -> h.getArgs().length > 0
|
||||
&& h.getArgs()[0].equalsIgnoreCase("Session")).toList();
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-15
@@ -1,4 +1,6 @@
|
||||
package com.bencodez.votingplugin.commands;
|
||||
package com.bencodez.votingplugin.tests.commands;
|
||||
|
||||
import com.bencodez.votingplugin.commands.CommandLoader;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -133,20 +135,25 @@ class OfflineVoteClearCommandRegistrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TestLoader extends CommandLoader {
|
||||
private final AtomicReference<java.util.function.BooleanSupplier> mutation;
|
||||
private static final class TestLoader {
|
||||
private final CommandLoader delegate;
|
||||
|
||||
TestLoader(VotingPluginMain plugin, AtomicReference<java.util.function.BooleanSupplier> mutation) {
|
||||
super(plugin);
|
||||
this.mutation = mutation;
|
||||
}
|
||||
TestLoader(VotingPluginMain plugin, AtomicReference<java.util.function.BooleanSupplier> mutation) {
|
||||
delegate = org.mockito.Mockito.mock(CommandLoader.class, org.mockito.Mockito.withSettings()
|
||||
.useConstructor(plugin).defaultAnswer(org.mockito.Mockito.CALLS_REAL_METHODS));
|
||||
CommandLoaderAccess.runForCommandSender(org.mockito.Mockito.doAnswer(invocation -> {
|
||||
invocation.getArgument(1, Runnable.class).run();
|
||||
return null;
|
||||
}).when(delegate), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
|
||||
CommandLoaderAccess.runConfirmedBulkStorageMutation(org.mockito.Mockito.doAnswer(invocation -> {
|
||||
mutation.set(invocation.getArgument(1));
|
||||
return null;
|
||||
}).when(delegate), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Override
|
||||
void runForCommandSender(CommandSender sender, Runnable task) { task.run(); }
|
||||
|
||||
@Override
|
||||
void runConfirmedBulkStorageMutation(CommandSender sender, java.util.function.BooleanSupplier work, Runnable success) {
|
||||
mutation.set(work);
|
||||
}
|
||||
}
|
||||
void registerOfflineVoteClearCommands(String permission) {
|
||||
CommandLoaderAccess.registerOfflineVoteClearCommands(delegate, permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.bencodez.votingplugin.tests.commands.gui.admin;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.commands.gui.admin.*;
|
||||
import com.bencodez.votingplugin.commands.gui.admin.AdminVotePerms.*;
|
||||
import java.util.ArrayList;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import com.bencodez.advancedcore.api.command.CommandHandler;
|
||||
|
||||
/** Typed test-only access to package-private AdminVotePerms seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class AdminVotePermsAccess {
|
||||
private AdminVotePermsAccess() { }
|
||||
public static ArrayList<String> additionalPermissions(CommandHandler handle) { return (ArrayList<String>) ReflectiveAccess.call("com.bencodez.votingplugin.commands.gui.admin.AdminVotePerms", null, "additionalPermissions", new String[]{"CommandHandler"}, new Object[]{handle}); }
|
||||
public static boolean hasEffectivePermission(CommandSender sender, String permission) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.commands.gui.admin.AdminVotePerms", null, "hasEffectivePermission", new String[]{"CommandSender", "String"}, new Object[]{sender, permission}); }
|
||||
}
|
||||
+8
-5
@@ -1,4 +1,6 @@
|
||||
package com.bencodez.votingplugin.commands.gui.admin;
|
||||
package com.bencodez.votingplugin.tests.commands.gui.admin;
|
||||
|
||||
import com.bencodez.votingplugin.commands.gui.admin.AdminVotePerms;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -37,14 +39,14 @@ class AdminVotePermsTest {
|
||||
"VotingPlugin.Commands.AdminVote.AddPoints.All");
|
||||
|
||||
assertEquals(Collections.singletonList("VotingPlugin.Commands.AdminVote.SetPoints.All"),
|
||||
AdminVotePerms.additionalPermissions(setPoints));
|
||||
AdminVotePermsAccess.additionalPermissions(setPoints));
|
||||
assertEquals(Collections.singletonList("VotingPlugin.Commands.AdminVote.AddPoints.All"),
|
||||
AdminVotePerms.additionalPermissions(addPoints));
|
||||
AdminVotePermsAccess.additionalPermissions(addPoints));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ordinaryCommandsDoNotInventAdditionalPermissions() {
|
||||
assertEquals(Collections.emptyList(), AdminVotePerms.additionalPermissions(mock(CommandHandler.class)));
|
||||
assertEquals(Collections.emptyList(), AdminVotePermsAccess.additionalPermissions(mock(CommandHandler.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -125,7 +127,7 @@ class AdminVotePermsTest {
|
||||
void editorPermissionStatusIncludesAdminOverride() {
|
||||
CommandSender sender = mock(CommandSender.class);
|
||||
when(sender.hasPermission(AdminAuthorization.ADMIN_PERMISSION)).thenReturn(true);
|
||||
assertTrue(AdminVotePerms.hasEffectivePermission(sender, "VotingPlugin.Commands.AdminVote.Edit.Config"));
|
||||
assertTrue(AdminVotePermsAccess.hasEffectivePermission(sender, "VotingPlugin.Commands.AdminVote.Edit.Config"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -215,6 +217,7 @@ class AdminVotePermsTest {
|
||||
when(plugin.getVoteCommand()).thenReturn(new ArrayList<>(vote));
|
||||
when(plugin.getAdminVoteCommand()).thenReturn(new ArrayList<>(admin));
|
||||
when(plugin.getConfigFile()).thenReturn(config);
|
||||
when(plugin.getVoteCommandPrefix()).thenReturn("/vote");
|
||||
when(plugin.getOptions()).thenReturn(options);
|
||||
when(options.isMultiplePermissionChecks()).thenReturn(multiplePermissions);
|
||||
when(config.getFormatPageSize()).thenReturn(pageSize);
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.bencodez.votingplugin.tests.commands.gui.player;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.commands.gui.player.*;
|
||||
import com.bencodez.votingplugin.commands.gui.player.VoteGUI.*;
|
||||
import org.bukkit.entity.Player;
|
||||
import com.bencodez.votingplugin.VotingPluginMain;
|
||||
import com.bencodez.votingplugin.user.VotingPluginUser;
|
||||
|
||||
/** Typed test-only access to package-private VoteGUI seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class VoteGUIAccess {
|
||||
private VoteGUIAccess() { }
|
||||
public static String getRequiredPermission(VotingPluginMain plugin, Player player, VotingPluginUser viewedUser) { return (String) ReflectiveAccess.call("com.bencodez.votingplugin.commands.gui.player.VoteGUI", null, "getRequiredPermission", new String[]{"VotingPluginMain", "Player", "VotingPluginUser"}, new Object[]{plugin, player, viewedUser}); }
|
||||
public static boolean isAuthorized(VotingPluginMain plugin, Player player, VotingPluginUser viewedUser) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.commands.gui.player.VoteGUI", null, "isAuthorized", new String[]{"VotingPluginMain", "Player", "VotingPluginUser"}, new Object[]{plugin, player, viewedUser}); }
|
||||
}
|
||||
+10
-8
@@ -1,4 +1,6 @@
|
||||
package com.bencodez.votingplugin.commands.gui.player;
|
||||
package com.bencodez.votingplugin.tests.commands.gui.player;
|
||||
|
||||
import com.bencodez.votingplugin.commands.gui.player.VoteGUI;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
@@ -36,7 +38,7 @@ class VoteGUIPermissionTest {
|
||||
|
||||
assertNotEquals(player.getUniqueId().toString(), viewedUser.getUUID());
|
||||
assertEquals("VotingPlugin.Commands.Vote.GUI",
|
||||
VoteGUI.getRequiredPermission(plugin, player, viewedUser));
|
||||
VoteGUIAccess.getRequiredPermission(plugin, player, viewedUser));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -53,7 +55,7 @@ class VoteGUIPermissionTest {
|
||||
when(viewedUser.getUUID()).thenReturn(null);
|
||||
|
||||
assertEquals("VotingPlugin.Commands.Vote.GUI.Other",
|
||||
VoteGUI.getRequiredPermission(plugin, player, viewedUser));
|
||||
VoteGUIAccess.getRequiredPermission(plugin, player, viewedUser));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -70,10 +72,10 @@ class VoteGUIPermissionTest {
|
||||
when(viewedUser.getUUID()).thenReturn("33333333-3333-3333-3333-333333333333");
|
||||
|
||||
assertEquals("VotingPlugin.Commands.Vote.GUI.Other",
|
||||
VoteGUI.getRequiredPermission(plugin, player, viewedUser));
|
||||
VoteGUIAccess.getRequiredPermission(plugin, player, viewedUser));
|
||||
when(player.hasPermission("VotingPlugin.Commands.Vote.GUI.Other")).thenReturn(false, true);
|
||||
assertFalse(VoteGUI.isAuthorized(plugin, player, viewedUser));
|
||||
assertTrue(VoteGUI.isAuthorized(plugin, player, viewedUser));
|
||||
assertFalse(VoteGUIAccess.isAuthorized(plugin, player, viewedUser));
|
||||
assertTrue(VoteGUIAccess.isAuthorized(plugin, player, viewedUser));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -89,8 +91,8 @@ class VoteGUIPermissionTest {
|
||||
when(viewedUser.getUUID()).thenReturn("target");
|
||||
when(player.hasPermission("VotingPlugin.Commands.Vote.GUI.Other")).thenReturn(true, false);
|
||||
|
||||
assertTrue(VoteGUI.isAuthorized(plugin, player, viewedUser), "dialog may initially open");
|
||||
assertFalse(VoteGUI.isAuthorized(plugin, player, viewedUser),
|
||||
assertTrue(VoteGUIAccess.isAuthorized(plugin, player, viewedUser), "dialog may initially open");
|
||||
assertFalse(VoteGUIAccess.isAuthorized(plugin, player, viewedUser),
|
||||
"dialog action must observe a permission revoked after opening");
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.bencodez.votingplugin.tests.commands.gui.player;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.commands.gui.player.*;
|
||||
import java.util.function.Predicate;
|
||||
import com.bencodez.votingplugin.voteshop.shop.VoteShopCategory;
|
||||
import com.bencodez.votingplugin.voteshop.shop.VoteShopCategoryButton;
|
||||
import com.bencodez.votingplugin.voteshop.shop.VoteShopDefinition;
|
||||
import com.bencodez.votingplugin.voteshop.shop.VoteShopItem;
|
||||
|
||||
/** Test-only forwarding access to the non-public production VoteShopCategoryAuthorization. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class VoteShopCategoryAuthorizationAccess implements ReflectiveAccess.View {
|
||||
private final Object delegate;
|
||||
private VoteShopCategoryAuthorizationAccess(Object delegate) { this.delegate = delegate; }
|
||||
public static VoteShopCategoryAuthorizationAccess view(Object delegate) { return delegate == null ? null : new VoteShopCategoryAuthorizationAccess(delegate); }
|
||||
@Override public Object delegate() { return delegate; }
|
||||
@Override public boolean equals(Object other) { return other instanceof VoteShopCategoryAuthorizationAccess value && delegate.equals(value.delegate); }
|
||||
@Override public int hashCode() { return delegate.hashCode(); }
|
||||
public static VoteShopCategoryAuthorizationAccess mock() { return view(org.mockito.Mockito.mock(ReflectiveAccess.type("com.bencodez.votingplugin.commands.gui.player.VoteShopCategoryAuthorization"))); }
|
||||
public VoteShopCategoryAuthorizationAccess verify() { return view(org.mockito.Mockito.verify(delegate)); }
|
||||
public VoteShopCategoryAuthorizationAccess verify(org.mockito.verification.VerificationMode mode) { return view(org.mockito.Mockito.verify(delegate, mode)); }
|
||||
public VoteShopCategoryAuthorizationAccess stub(org.mockito.stubbing.Stubber stubber) { return view(stubber.when(delegate)); }
|
||||
public static boolean mayDisplay(VoteShopCategoryButton button, Predicate<String> permissionCheck) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.commands.gui.player.VoteShopCategoryAuthorization", null, "mayDisplay", new String[]{"VoteShopCategoryButton", "Predicate"}, new Object[]{button, permissionCheck}); }
|
||||
public static boolean authorized(VoteShopCategoryButton button, Predicate<String> permissionCheck) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.commands.gui.player.VoteShopCategoryAuthorization", null, "authorized", new String[]{"VoteShopCategoryButton", "Predicate"}, new Object[]{button, permissionCheck}); }
|
||||
public static boolean canOpen(VoteShopDefinition definition, String targetCategory, Predicate<String> permissionCheck) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.commands.gui.player.VoteShopCategoryAuthorization", null, "canOpen", new String[]{"VoteShopDefinition", "String", "Predicate"}, new Object[]{definition, targetCategory, permissionCheck}); }
|
||||
public static boolean canPurchase(VoteShopDefinition definition, VoteShopCategory category, VoteShopItem item, Predicate<String> permissionCheck) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.commands.gui.player.VoteShopCategoryAuthorization", null, "canPurchase", new String[]{"VoteShopDefinition", "VoteShopCategory", "VoteShopItem", "Predicate"}, new Object[]{definition, category, item, permissionCheck}); }
|
||||
}
|
||||
+16
-14
@@ -1,4 +1,6 @@
|
||||
package com.bencodez.votingplugin.commands.gui.player;
|
||||
package com.bencodez.votingplugin.tests.commands.gui.player;
|
||||
|
||||
import com.bencodez.votingplugin.tests.commands.gui.player.VoteShopCategoryAuthorizationAccess;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -20,17 +22,17 @@ class VoteShopCategoryAuthorizationTest {
|
||||
VoteShopCategoryButton button = button("restricted", "shop.category", false);
|
||||
VoteShopDefinition definition = definition(button);
|
||||
|
||||
assertTrue(VoteShopCategoryAuthorization.mayDisplay(button, permission -> false));
|
||||
assertFalse(VoteShopCategoryAuthorization.authorized(button, permission -> false));
|
||||
assertFalse(VoteShopCategoryAuthorization.canOpen(definition, "restricted", permission -> false));
|
||||
assertTrue(VoteShopCategoryAuthorizationAccess.mayDisplay(button, permission -> false));
|
||||
assertFalse(VoteShopCategoryAuthorizationAccess.authorized(button, permission -> false));
|
||||
assertFalse(VoteShopCategoryAuthorizationAccess.canOpen(definition, "restricted", permission -> false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hideFlagControlsVisibilityWithoutChangingAuthorization() {
|
||||
VoteShopCategoryButton hidden = button("restricted", "shop.category", true);
|
||||
|
||||
assertFalse(VoteShopCategoryAuthorization.mayDisplay(hidden, permission -> false));
|
||||
assertTrue(VoteShopCategoryAuthorization.mayDisplay(hidden, permission -> true));
|
||||
assertFalse(VoteShopCategoryAuthorizationAccess.mayDisplay(hidden, permission -> false));
|
||||
assertTrue(VoteShopCategoryAuthorizationAccess.mayDisplay(hidden, permission -> true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -46,11 +48,11 @@ class VoteShopCategoryAuthorizationTest {
|
||||
child.setId("child");
|
||||
definition.getCategories().put("child", child);
|
||||
|
||||
assertFalse(VoteShopCategoryAuthorization.canOpen(definition, "child",
|
||||
assertFalse(VoteShopCategoryAuthorizationAccess.canOpen(definition, "child",
|
||||
Set.of("shop.child")::contains));
|
||||
assertFalse(VoteShopCategoryAuthorization.canOpen(definition, "child",
|
||||
assertFalse(VoteShopCategoryAuthorizationAccess.canOpen(definition, "child",
|
||||
Set.of("shop.parent")::contains));
|
||||
assertTrue(VoteShopCategoryAuthorization.canOpen(definition, "child",
|
||||
assertTrue(VoteShopCategoryAuthorizationAccess.canOpen(definition, "child",
|
||||
Set.of("shop.parent", "shop.child")::contains));
|
||||
}
|
||||
|
||||
@@ -63,14 +65,14 @@ class VoteShopCategoryAuthorizationTest {
|
||||
category.getEntries().put("item", item);
|
||||
AtomicBoolean permitted = new AtomicBoolean(true);
|
||||
|
||||
assertTrue(VoteShopCategoryAuthorization.canPurchase(definition, category, item,
|
||||
assertTrue(VoteShopCategoryAuthorizationAccess.canPurchase(definition, category, item,
|
||||
permission -> permitted.get()));
|
||||
permitted.set(false);
|
||||
assertFalse(VoteShopCategoryAuthorization.canPurchase(definition, category, item,
|
||||
assertFalse(VoteShopCategoryAuthorizationAccess.canPurchase(definition, category, item,
|
||||
permission -> permitted.get()));
|
||||
VoteShopItem mainItem = item("main");
|
||||
definition.getMainEntries().put("main", mainItem);
|
||||
assertTrue(VoteShopCategoryAuthorization.canPurchase(definition, null, mainItem, permission -> false));
|
||||
assertTrue(VoteShopCategoryAuthorizationAccess.canPurchase(definition, null, mainItem, permission -> false));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -82,7 +84,7 @@ class VoteShopCategoryAuthorizationTest {
|
||||
|
||||
definition.getCategories().remove("restricted");
|
||||
|
||||
assertFalse(VoteShopCategoryAuthorization.canPurchase(definition, staleCategory, item,
|
||||
assertFalse(VoteShopCategoryAuthorizationAccess.canPurchase(definition, staleCategory, item,
|
||||
permission -> true));
|
||||
}
|
||||
|
||||
@@ -97,7 +99,7 @@ class VoteShopCategoryAuthorizationTest {
|
||||
replacement.getEntries().put("item", item("item"));
|
||||
definition.getCategories().put("restricted", replacement);
|
||||
|
||||
assertFalse(VoteShopCategoryAuthorization.canPurchase(definition, staleCategory, staleItem,
|
||||
assertFalse(VoteShopCategoryAuthorizationAccess.canPurchase(definition, staleCategory, staleItem,
|
||||
permission -> true));
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.bencodez.votingplugin.tests.commands.gui.player;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.commands.gui.player.*;
|
||||
import com.bencodez.votingplugin.commands.gui.player.VoteShopConfirm.*;
|
||||
|
||||
/** Typed test-only access to package-private VoteShopConfirm seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class VoteShopConfirmAccess {
|
||||
private VoteShopConfirmAccess() { }
|
||||
public static boolean beginPurchase(VoteShopConfirm target) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.commands.gui.player.VoteShopConfirm", target, "beginPurchase", new String[]{}, new Object[]{}); }
|
||||
}
|
||||
+5
-3
@@ -1,4 +1,6 @@
|
||||
package com.bencodez.votingplugin.commands.gui.player;
|
||||
package com.bencodez.votingplugin.tests.commands.gui.player;
|
||||
|
||||
import com.bencodez.votingplugin.commands.gui.player.VoteShopConfirm;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -10,7 +12,7 @@ class VoteShopConfirmTest {
|
||||
void confirmationCanSubmitOnlyOnePurchase() {
|
||||
VoteShopConfirm confirmation = new VoteShopConfirm(null, null, null, null, null);
|
||||
|
||||
assertTrue(confirmation.beginPurchase());
|
||||
assertFalse(confirmation.beginPurchase());
|
||||
assertTrue(VoteShopConfirmAccess.beginPurchase(confirmation));
|
||||
assertFalse(VoteShopConfirmAccess.beginPurchase(confirmation));
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.bencodez.votingplugin.tests.control;
|
||||
|
||||
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
|
||||
import com.bencodez.votingplugin.control.*;
|
||||
import com.bencodez.votingplugin.control.BackendConfigurationService.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Typed test-only access to package-private BackendConfigurationService seams. */
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public final class BackendConfigurationServiceAccess {
|
||||
public static final int MAX_REWARD_DIRECTORY_ENTRIES = (Integer) ReflectiveAccess.field("com.bencodez.votingplugin.control.BackendConfigurationService", "MAX_REWARD_DIRECTORY_ENTRIES");
|
||||
private BackendConfigurationServiceAccess() { }
|
||||
public static BackendConfigurationService create(Path dataDirectory, ReloadAction reload) { return (BackendConfigurationService) ReflectiveAccess.construct("com.bencodez.votingplugin.control.BackendConfigurationService", new String[]{"Path", "ReloadAction"}, new Object[]{dataDirectory, reload}); }
|
||||
public static BackendConfigurationService create(Path dataDirectory, ApplyAction reload) { return (BackendConfigurationService) ReflectiveAccess.construct("com.bencodez.votingplugin.control.BackendConfigurationService", new String[]{"Path", "ApplyAction"}, new Object[]{dataDirectory, reload}); }
|
||||
public static BackendConfigurationService create(Path dataDirectory, ApplyAction reload, BackendConfigurationServiceAccess.MoveAction mover) { return (BackendConfigurationService) ReflectiveAccess.construct("com.bencodez.votingplugin.control.BackendConfigurationService", new String[]{"Path", "ApplyAction", "MoveAction"}, new Object[]{dataDirectory, reload, mover}); }
|
||||
public static boolean supportsNamedRewardFiles(BackendConfigurationService target) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", target, "supportsNamedRewardFiles", new String[]{}, new Object[]{}); }
|
||||
public static boolean supportsNamedRewardFiles(BackendConfigurationService target, BackendConfigurationServiceAccess.DirectoryStagingSupport staging) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", target, "supportsNamedRewardFiles", new String[]{"DirectoryStagingSupport"}, new Object[]{staging}); }
|
||||
public static boolean supportsNamedRewardFiles(Path directory) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", null, "supportsNamedRewardFiles", new String[]{"Path"}, new Object[]{directory}); }
|
||||
public static boolean supportsNamedRewardFiles(Path directory, BackendConfigurationServiceAccess.DirectoryForcer force) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", null, "supportsNamedRewardFiles", new String[]{"Path", "DirectoryForcer"}, new Object[]{directory, force}); }
|
||||
public static boolean supportsNamedRewardFiles(Path directory, BackendConfigurationServiceAccess.DirectoryForcer force, BackendConfigurationServiceAccess.DirectoryStagingSupport staging) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", null, "supportsNamedRewardFiles", new String[]{"Path", "DirectoryForcer", "DirectoryStagingSupport"}, new Object[]{directory, force, staging}); }
|
||||
public static List<String> rewardFileInventory(Path dataDirectory) throws IOException { return (List<String>) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", null, "rewardFileInventory", new String[]{"Path"}, new Object[]{dataDirectory}); }
|
||||
public static List<String> rewardFileInventory(Path dataDirectory, BackendConfigurationServiceAccess.RewardDirectoryAction<List<String>> scan) throws IOException { return (List<String>) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", null, "rewardFileInventory", new String[]{"Path", "RewardDirectoryAction"}, new Object[]{dataDirectory, scan}); }
|
||||
public static String readRaw(java.nio.file.SecureDirectoryStream<Path> directory, String name, boolean allowMissing) throws IOException { return (String) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", null, "readRaw", new String[]{"SecureDirectoryStream", "String", "boolean"}, new Object[]{directory, name, allowMissing}); }
|
||||
public static boolean namedRewardCannotBeConfirmed(IOException failure) { return (Boolean) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", null, "namedRewardCannotBeConfirmed", new String[]{"IOException"}, new Object[]{failure}); }
|
||||
public static <T> T retryRead(BackendConfigurationServiceAccess.ReadAction<T> read) throws IOException { return (T) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", null, "retryRead", new String[]{"ReadAction"}, new Object[]{read}); }
|
||||
public static String proposedQuickSetupRevision(BackendConfigurationService target, String preset, QuickPreview preview) throws IOException { return (String) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", target, "proposedQuickSetupRevision", new String[]{"String", "QuickPreview"}, new Object[]{preset, preview}); }
|
||||
public static String currentQuickSetupRevision(BackendConfigurationService target, String preset) throws IOException { return (String) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", target, "currentQuickSetupRevision", new String[]{"String"}, new Object[]{preset}); }
|
||||
public static String currentQuickSetupRevision(BackendConfigurationService target, String preset, Map<String, String> options) throws IOException { return (String) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", target, "currentQuickSetupRevision", new String[]{"String", "Map"}, new Object[]{preset, options}); }
|
||||
public static ApplyResult applyQuickSetup(BackendConfigurationService target, String preset, Map<String, String> options, String expectedRevision) throws IOException { return (ApplyResult) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", target, "applyQuickSetup", new String[]{"String", "Map", "String"}, new Object[]{preset, options, expectedRevision}); }
|
||||
public static ApplyResult applyQuickSetup(BackendConfigurationService target, String preset, Map<String, String> options, String expectedRevision, ApplyAction applyAction) throws IOException { return (ApplyResult) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", target, "applyQuickSetup", new String[]{"String", "Map", "String", "ApplyAction"}, new Object[]{preset, options, expectedRevision, applyAction}); }
|
||||
public static List<String> sanitizeComments(List<String> comments, List<String> secretValues, boolean secretPath) { return (List<String>) ReflectiveAccess.call("com.bencodez.votingplugin.control.BackendConfigurationService", null, "sanitizeComments", new String[]{"List", "List", "boolean"}, new Object[]{comments, secretValues, secretPath}); }
|
||||
@FunctionalInterface
|
||||
public interface ReadAction<T> {
|
||||
T run() throws IOException;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DirectoryForcer {
|
||||
void force(FileChannel channel) throws IOException;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DirectoryStagingSupport {
|
||||
boolean supports(java.nio.file.SecureDirectoryStream<Path> directory) throws IOException;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RewardDirectoryAction<T> {
|
||||
T run(java.nio.file.SecureDirectoryStream<Path> directory) throws IOException;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface MoveAction {
|
||||
void move(Path source, Path target) throws IOException;
|
||||
}
|
||||
|
||||
}
|
||||
Loaded 100 of 279 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user