Compare commits

..
248 changed files with 5360 additions and 9854 deletions

No files matched your search

-6
View File
@@ -26,8 +26,6 @@ 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:
@@ -60,10 +58,6 @@ 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.
+6 -6
View File
@@ -382,7 +382,7 @@
<dependency>
<groupId>net.neoforged</groupId>
<artifactId>mergetool</artifactId>
<version>2.0.7</version>
<version>2.0.0</version>
<classifier>api</classifier>
<scope>provided</scope>
</dependency>
@@ -406,7 +406,7 @@
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>26.3-R0.1-SNAPSHOT</version>
<version>26.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
<exclusions>
<exclusion>
@@ -475,7 +475,7 @@
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>8.0.1</version>
<version>7.5.3</version>
</dependency>
<dependency>
<groupId>me.clip</groupId>
@@ -525,7 +525,7 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>6.1.3</version>
<version>5.12.2</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -537,13 +537,13 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>6.1.3</version>
<version>5.12.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.24.0</version>
<version>5.23.0</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -71,9 +71,11 @@ 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;
@@ -85,9 +87,6 @@ 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;
@@ -157,6 +156,49 @@ import lombok.Setter;
public class VotingPluginMain extends AdvancedCorePlugin {
private final Object votingNPCLifecycle = new Object();
private final java.util.concurrent.atomic.AtomicLong votingNPCConfiguration = new java.util.concurrent.atomic.AtomicLong();
private volatile com.bencodez.votingplugin.npc.VotingNPCManager votingNPCManager;
public com.bencodez.votingplugin.npc.VotingNPCManager getVotingNPCManager() { return votingNPCManager; }
/** Optional NPC storage is lazy and asynchronous; configuration only schedules owner work. */
private void refreshVotingNPCs() {
boolean requested = getConfigFile().isVotingNPCsEnabled();
long generation = votingNPCConfiguration.incrementAndGet();
if (!requested && votingNPCManager == null) return;
getBukkitScheduler().runTask(this, () -> {
com.bencodez.votingplugin.npc.VotingNPCManager manager;
synchronized (votingNPCLifecycle) {
if (!isEnabled() || generation != votingNPCConfiguration.get()) return;
if (votingNPCManager == null && requested) {
try {
var created = new com.bencodez.votingplugin.npc.VotingNPCManager(this);
Bukkit.getPluginManager().registerEvents(created, this);
votingNPCManager = created;
} catch (RuntimeException | LinkageError unsupported) {
getLogger().warning("Voting NPCs are unavailable on this server; normal voting is unaffected.");
return;
}
}
manager = votingNPCManager;
}
if (manager != null) manager.configure(requested);
});
}
private void closeVotingNPCs() {
com.bencodez.votingplugin.npc.VotingNPCManager manager;
synchronized (votingNPCLifecycle) {
votingNPCConfiguration.incrementAndGet();
manager = votingNPCManager;
votingNPCManager = null;
}
if (manager != null) {
try { manager.close(); } catch (RuntimeException | LinkageError failure) { debug(failure); }
}
}
@Getter
public static VotingPluginMain plugin;
@@ -175,9 +217,6 @@ public class VotingPluginMain extends AdvancedCorePlugin {
@Getter
private BroadcastHandler broadcastHandler;
@Getter
private com.bencodez.votingplugin.streaks.VoteStreaksMenu voteStreaksMenu;
@Getter
private BackendProxyHandler backendProxyHandler;
private ProcessedVoteCache backendProcessedVoteCache;
@@ -217,21 +256,6 @@ 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;
@@ -315,8 +339,6 @@ public class VotingPluginMain extends AdvancedCorePlugin {
@Getter
private ScheduledExecutorService voteTimer;
private volatile AutoCreateVoteSitesReminderService autoCreateVoteSitesSetupReminder;
private ScheduledExecutorService autoCreateVoteSitesReminderWorker;
@Getter
private UserManager votingPluginUserManager;
@@ -743,16 +765,6 @@ 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()) {
@@ -779,6 +791,7 @@ public class VotingPluginMain extends AdvancedCorePlugin {
checkVotifier();
registerEvents();
refreshPlaceholderPlayerPresence();
refreshVotingNPCs();
loadVoteBroadcast();
@@ -1975,7 +1988,6 @@ public class VotingPluginMain extends AdvancedCorePlugin {
@Override
public void onDisable() {
stopAutoCreateVoteSitesSetupReminder();
if (offlineVoteOwnerHandoffs != null) offlineVoteOwnerHandoffs.close();
try {
shutdownVoteReminders();
@@ -1984,24 +1996,6 @@ public class VotingPluginMain extends AdvancedCorePlugin {
}
}
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();
@@ -2011,8 +2005,7 @@ public class VotingPluginMain extends AdvancedCorePlugin {
@Override
public void onUnLoad() {
if (voteStreaksMenu != null) voteStreaksMenu.close();
stopAutoCreateVoteSitesSetupReminder();
closeVotingNPCs();
placeholderPlayerPresence.clear();
stopBackendHostedControlLifecycle();
stopBackendControlConnectorLifecycle();
@@ -2065,6 +2058,12 @@ 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());
@@ -2091,8 +2090,6 @@ 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);
@@ -2173,11 +2170,8 @@ public class VotingPluginMain extends AdvancedCorePlugin {
boolean updateActiveBackendRuntime) {
if (offlineVoteOwnerHandoffs != null) offlineVoteOwnerHandoffs.pause();
try {
if (voteStreaksMenu != null) voteStreaksMenu.suspend();
configFile.reloadData();
configFile.loadValues();
AutoCreateVoteSitesReminderService setupReminder = autoCreateVoteSitesSetupReminder;
if (setupReminder != null) setupReminder.reload(autoCreateVoteSitesReminderSettings());
configVoteSites.reloadData();
@@ -2223,11 +2217,11 @@ public class VotingPluginMain extends AdvancedCorePlugin {
loadDirectlyDefined();
refreshVotingNPCs();
if (reconcileHostedControl) restartBackendControlConnector();
setUpdate(true);
} finally {
if (voteStreaksMenu != null) voteStreaksMenu.resume();
if (offlineVoteOwnerHandoffs != null) offlineVoteOwnerHandoffs.resume();
}
}
@@ -56,7 +56,6 @@ 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;
@@ -83,7 +82,6 @@ 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;
@@ -117,13 +115,6 @@ 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.
@@ -132,131 +123,6 @@ 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) {
@@ -537,28 +403,26 @@ 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());
}
for (var entry : commands.entrySet()) {
if (entry.getKey().equalsIgnoreCase(cmd)) return entry.getValue().hasPerm(player);
if (commands.containsKey(cmd)) {
return commands.get(cmd).hasPerm(player);
}
boolean adminCommand = false;
String rootLabel = voteCommandName.substring(voteCommandName.indexOf(':') + 1);
if (cmd.equals("adminvote") || cmd.startsWith("adminvote ")) {
if (cmd.startsWith("vote")) {
cmd = cmd.substring("vote".length());
} else if (cmd.startsWith("v")) {
cmd = cmd.substring(1);
} else if (cmd.startsWith("av")) {
adminCommand = true;
cmd = cmd.substring("adminvote".length()).trim();
} else if (cmd.equals("av") || cmd.startsWith("av ")) {
cmd = cmd.substring("av".length());
} else if (cmd.startsWith("adminvote")) {
adminCommand = true;
cmd = cmd.substring("av".length()).trim();
} else if (voteCommandEnabled && (cmd.equals(rootLabel) || cmd.startsWith(rootLabel + " "))) {
cmd = cmd.substring(rootLabel.length()).trim();
} else {
return false;
cmd = cmd.substring("adminvote".length());
}
// plugin.debug(cmd);
if (!adminCommand) {
@@ -595,11 +459,13 @@ public class CommandLoader {
* @return true if it's a VotingPlugin command
*/
public boolean isVotingPluginCommand(Player player, String cmd) {
try {
org.bukkit.command.Command command = getCommandMap().getCommand(cmd);
return command instanceof PluginCommand owned && owned.getPlugin() == plugin;
} catch (ReflectiveOperationException failure) {
plugin.debug(failure);
if (plugin.getCommand(cmd) != null || cmd.startsWith("votingplugin")) {
return true;
}
for (String str : commands.keySet()) {
if (str.equalsIgnoreCase(cmd)) {
return true;
}
}
return false;
}
@@ -609,6 +475,7 @@ public class CommandLoader {
*/
private void loadAdminVoteCommand() {
plugin.setAdminVoteCommand(new ArrayList<>());
com.bencodez.votingplugin.npc.VotingNPCCommands.register(plugin);
plugin.getAdminVoteCommand().add(new CommandHandler(plugin, new String[] { "CurrentPluginTime" },
"VotingPlugin.Commands.AdminVote.CurrentPluginTime|" + adminPerm, "Current plugin time") {
@@ -3103,10 +2970,9 @@ public class CommandLoader {
public void loadAliases() {
commands = new HashMap<>();
aliasCommandNames.clear();
configurePlayerCommand();
// Bukkit registers plugin.yml commands before this loader runs.
if (!startupLoadCommandAliases) {
if (!plugin.getConfigFile().isLoadCommandAliases()) {
unregisterOptionalPluginYMLCommands();
} else {
unregisterDisabledPluginYMLCommands();
@@ -3114,7 +2980,7 @@ public class CommandLoader {
// If false: still wire permissions, but don't wire alias executors/tab
// completers.
final boolean enableAliases = startupLoadCommandAliases;
final boolean enableAliases = plugin.getConfigFile().isLoadCommandAliases();
// ---------------------------
// /vote aliases
@@ -3146,18 +3012,16 @@ public class CommandLoader {
}
registerAdditionalPermissions(Bukkit.getPluginManager(), cmdHandle);
if (argLength > 0 && voteCommandEnabled) {
if (argLength > 0) {
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 = ownedCommand(commandName);
PluginCommand command = plugin.getCommand(commandName);
if (command == null) {
plugin.devDebug("Command " + commandName + " not found in plugin.yml");
continue;
@@ -3231,13 +3095,11 @@ 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 = ownedCommand(commandName);
PluginCommand command = plugin.getCommand(commandName);
if (command == null) {
plugin.devDebug("Command " + commandName + " not found in plugin.yml");
continue;
@@ -3303,8 +3165,8 @@ public class CommandLoader {
|| commandName.equalsIgnoreCase("av")) {
continue;
}
PluginCommand command = ownedCommand(commandName);
if (command != null && command != playerRootCommand) {
PluginCommand command = plugin.getCommand(commandName);
if (command != null) {
unregisterCommand(commandMap, command);
}
}
@@ -3318,8 +3180,8 @@ public class CommandLoader {
try {
CommandMap commandMap = getCommandMap();
for (String commandName : disabledAliasCommandNames) {
PluginCommand command = ownedCommand(commandName);
if (command != null && command != playerRootCommand) {
PluginCommand command = plugin.getCommand(commandName);
if (command != null) {
unregisterCommand(commandMap, command);
}
}
@@ -3331,7 +3193,22 @@ 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);
Map<String, org.bukkit.command.Command> knownCommands = getKnownCommands(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);
Set<String> removedLabels = new HashSet<>();
for (Map.Entry<String, org.bukkit.command.Command> entry : knownCommands.entrySet()) {
if (entry.getValue() == command) {
@@ -3360,27 +3237,6 @@ 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");
@@ -3444,6 +3300,14 @@ public class CommandLoader {
* Load tab complete.
*/
public void loadTabComplete() {
TabCompleteHandler.getInstance().addTabCompleteOption(new TabCompleteHandle("(npcid)", new ArrayList<>()) {
@Override public void reload() { updateReplacements(); }
@Override public void updateReplacements() {
var manager = plugin.getVotingNPCManager();
setReplace(manager == null ? new ArrayList<>() : new ArrayList<>(manager.definitions().stream()
.map(com.bencodez.votingplugin.npc.VotingNPCDefinition::id).toList()));
}
});
ArrayList<String> sites = new ArrayList<>();
for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) {
sites.add(site.getKey());
@@ -4214,9 +4078,6 @@ 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) {
@@ -4283,7 +4144,7 @@ public class CommandLoader {
} else if (MessageAPI.startsWithIgnoreCase(slot, "today")) {
new VoteToday(plugin, player, user, 1).open();
} else if (MessageAPI.startsWithIgnoreCase(slot, "help")) {
performVoteCommand(player, "help");
player.performCommand("vote help");
} else if (MessageAPI.startsWithIgnoreCase(slot, "shop")) {
new VoteShop(plugin, player, user).open();
} else if (MessageAPI.startsWithIgnoreCase(slot, "lastmonth")) {
@@ -4292,8 +4153,6 @@ 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);
}
}
@@ -74,7 +74,7 @@ public class CommandAliases implements CommandExecutor {
if (adminCommand) {
sender.sendMessage(MessageAPI.colorize(plugin.getConfigFile().getFormatInvalidCommandAdminVote()));
} else {
sender.sendMessage(MessageAPI.colorize(plugin.formatVoteCommandMessage(plugin.getConfigFile().getFormatInvalidCommandVote())));
sender.sendMessage(MessageAPI.colorize(plugin.getConfigFile().getFormatInvalidCommandVote()));
}
return true;
}
@@ -35,7 +35,7 @@ public class CommandVote implements CommandExecutor {
}
// invalid command
sender.sendMessage(MessageAPI.colorize(plugin.formatVoteCommandMessage(plugin.getConfigFile().getFormatInvalidCommandVote())));
sender.sendMessage(MessageAPI.colorize(plugin.getConfigFile().getFormatInvalidCommandVote()));
return true;
}
@@ -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(plugin.getVoteCommandPrefix()) + " : "
msg.add("&6" + handle.getHelpLineCommand("/vote") + " : "
+ handle.getPerm().split(Pattern.quote("|"))[0] + " : &atrue");
} else {
msg.add("&6" + handle.getHelpLineCommand(plugin.getVoteCommandPrefix()) + " : "
msg.add("&6" + handle.getHelpLineCommand("/vote") + " : "
+ handle.getPerm().split(Pattern.quote("|"))[0] + " : &cfalse");
}
} else {
msg.add(handle.getHelpLineCommand(plugin.getVoteCommandPrefix()) + " : " + handle.getPerm().split(Pattern.quote("|"))[0]);
msg.add(handle.getHelpLineCommand("/vote") + " : " + handle.getPerm().split(Pattern.quote("|"))[0]);
}
addAdditionalPermissions(msg, sender, handle, plugin.getVoteCommandPrefix());
addAdditionalPermissions(msg, sender, handle, "/vote");
}
@@ -164,13 +164,13 @@ public class AdminVotePerms extends GUIHandler {
for (CommandHandler handle : plugin.getVoteCommand()) {
if (handle.hasPerm(p)) {
msg.add("&6" + handle.getHelpLineCommand(plugin.getVoteCommandPrefix()) + " : "
msg.add("&6" + handle.getHelpLineCommand("/vote") + " : "
+ handle.getPerm().split(Pattern.quote("|"))[0] + " : &atrue");
} else {
msg.add("&6" + handle.getHelpLineCommand(plugin.getVoteCommandPrefix()) + " : "
msg.add("&6" + handle.getHelpLineCommand("/vote") + " : "
+ handle.getPerm().split(Pattern.quote("|"))[0] + " : &cfalse");
}
addAdditionalPermissions(msg, p, handle, plugin.getVoteCommandPrefix());
addAdditionalPermissions(msg, p, handle, "/vote");
}
@@ -237,7 +237,7 @@ public class AdminVotePerms extends GUIHandler {
msg.add(" ");
for (CommandHandler handle : plugin.getVoteCommand()) {
msg.add(handle.getHelpLineCommand(plugin.getVoteCommandPrefix()));
msg.add(handle.getHelpLineCommand("/vote"));
msg.add(" " + handle.getPerm());
msg.add(" " + handle.getHelpMessage());
for (String permission : additionalPermissions(handle)) {
@@ -55,7 +55,6 @@ 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();
@@ -71,7 +70,7 @@ public class VoteHelp extends GUIHandler {
}
for (CommandHandler cmdHandle : plugin.getVoteCommand()) {
if (!requirePerms || cmdHandle.hasPerm(sender)) {
unsorted.put(cmdHandle.getHelpLineCommand(plugin.getVoteCommandPrefix()), cmdHandle.getHelpLine(plugin.getVoteCommandPrefix(),
unsorted.put(cmdHandle.getHelpLineCommand("/vote"), cmdHandle.getHelpLine("/vote",
plugin.getConfigFile().getFormatCommandsVoteHelpLine(), hoverColor));
}
}
@@ -93,15 +92,14 @@ 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(plugin.getVoteCommandPrefix()),
cmdHandle.getHelpLine(plugin.getVoteCommandPrefix(), plugin.getConfigFile().getFormatCommandsVoteHelpLine()));
unsorted.put(cmdHandle.getHelpLineCommand("/vote"),
cmdHandle.getHelpLine("/vote", plugin.getConfigFile().getFormatCommandsVoteHelpLine()));
}
}
ArrayList<String> unsortedList = new ArrayList<>();
@@ -177,7 +177,7 @@ public class VoteURL extends GUIHandler {
}
if (plugin.getGui().isChestVoteURLBackButton()) {
dialog.button("&eBack", "&7Return to previous menu", payload -> plugin.getCommandLoader().performVoteCommand(player, "gui"));
dialog.button("&eBack", "&7Return to previous menu", payload -> player.performCommand("vote gui"));
}
dialog.open();
@@ -24,6 +24,9 @@ import com.bencodez.votingplugin.topvoter.TopVoter;
import lombok.Getter;
public class Config extends YMLFile {
/** Opt-in persistent vanilla voting NPCs; missing configuration preserves legacy behavior. */
public boolean isVotingNPCsEnabled() { return getData().getBoolean("VotingNPCs.Enabled", false); }
/** Explicit permission for executable deployment over private-network plaintext HTTP. */
public boolean getControlAllowInsecureHttpPluginDeployment() {
@@ -522,15 +525,6 @@ 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,10 +26,6 @@ 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";
@@ -1,127 +0,0 @@
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;
}
}
@@ -1,212 +0,0 @@
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;
}
}
@@ -1,240 +0,0 @@
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);
}
}
}
@@ -0,0 +1,119 @@
package com.bencodez.votingplugin.npc;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.CompletionStage;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.bencodez.advancedcore.api.command.CommandHandler;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.util.BukkitCompletionScheduler;
/** Normal /av command handlers; no separate command map or player GUI registry. */
public final class VotingNPCCommands {
private static final String PERMISSION = "VotingPlugin.Commands.AdminVote.NPC";
private VotingNPCCommands() { }
public static void register(VotingPluginMain plugin) {
add(plugin, new String[] { "NPC", "Create", "(string)" }, false);
add(plugin, new String[] { "NPC", "Create", "(string)", "(list)" }, false);
add(plugin, new String[] { "NPC", "List" }, true);
add(plugin, new String[] { "NPC", "Inspect", "(npcid)" }, true);
add(plugin, new String[] { "NPC", "Remove", "(npcid)" }, true);
}
private static void add(VotingPluginMain plugin, String[] pattern, boolean console) {
plugin.getAdminVoteCommand().add(new CommandHandler(plugin, pattern, PERMISSION + "|VotingPlugin.Admin",
"Manage saved vanilla voting NPCs", console) {
@Override public void execute(CommandSender sender, String[] args) {
// CommandHandler invokes execute asynchronously. Capture location,
// permission and messages only after reaching the sender's owner.
String[] captured = args.clone();
onSender(plugin, sender, () -> executeOwned(plugin, sender, captured));
}
});
}
private static boolean allowed(CommandSender sender) {
return (!(sender instanceof Player player) || player.isOnline())
&& (sender.hasPermission(PERMISSION) || sender.hasPermission("VotingPlugin.Admin"));
}
private static void onSender(VotingPluginMain plugin, CommandSender sender, Runnable action) {
Player player = sender instanceof Player candidate ? candidate : null;
BukkitCompletionScheduler.run(plugin, player, action, player == null ? action : () -> { }, () -> { });
}
private static void executeOwned(VotingPluginMain plugin, CommandSender sender, String[] args) {
if (!allowed(sender)) return;
VotingNPCManager manager = plugin.getVotingNPCManager();
if (manager == null || !manager.available()) {
sender.sendMessage("Voting NPCs are disabled, unsupported, or waiting for valid storage. Check VotingNPCs.Enabled in Config.yml and reload.");
return;
}
switch (args[1].toLowerCase(java.util.Locale.ROOT)) {
case "create":
if (!(sender instanceof Player player)) { sender.sendMessage("Create a voting NPC from in game."); return; }
try {
Location at = player.getLocation();
String name = args.length > 3 ? String.join(" ", Arrays.copyOfRange(args, 3, args.length)) : "&aVote";
VotingNPCDefinition definition = new VotingNPCDefinition(args[2], UUID.randomUUID(), at.getWorld().getUID(),
at.getX(), at.getY(), at.getZ(), at.getYaw(), at.getPitch(), name);
confirm(plugin, sender, manager, manager.create(definition),
ignored -> "Saved voting NPC '" + definition.id() + "'. Its villager appears when the area is loaded and available.");
} catch (IllegalArgumentException invalid) {
sender.sendMessage("Use a lowercase ID of 1-40 letters, digits, underscores or hyphens, and a name of 1-128 characters without controls.");
}
break;
case "remove":
confirm(plugin, sender, manager, manager.remove(args[2]), removed -> removed
? "Removed saved voting NPC '" + args[2] + "'; exact entity cleanup has been requested."
: "No voting NPC has that ID.");
break;
case "inspect":
manager.definitions().stream().filter(value -> value.id().equals(args[2])).findFirst().ifPresentOrElse(value ->
sender.sendMessage("Voting NPC " + value.id() + ": world " + value.world() + ", "
+ value.x() + ", " + value.y() + ", " + value.z() + "; " + manager.runtimeState(value.id())),
() -> sender.sendMessage("No voting NPC has that ID."));
break;
case "list":
sender.sendMessage("Saved voting NPCs: " + manager.definitions().size() + "/64; tracked runtime slots: " + manager.runtimeCount());
for (var value : manager.definitions()) sender.sendMessage(value.id() + ": " + manager.runtimeState(value.id()));
break;
default:
sender.sendMessage("Use /av npc create <id> [name], list, inspect <id>, or remove <id>.");
}
}
private static String failureMessage(VotingNPCManager manager, Throwable failure) {
while ((failure instanceof java.util.concurrent.CompletionException
|| failure instanceof java.util.concurrent.ExecutionException) && failure.getCause() != null) {
failure = failure.getCause();
}
if (manager.available()) {
if (failure instanceof VotingNPCRegistry.ValidationFailure validation) {
return switch (validation.reason) {
case DUPLICATE_ID -> "That voting NPC ID already exists. Use a different ID or remove it first.";
case DUPLICATE_IDENTITY -> "That voting NPC identity already exists. No definition was changed.";
case LIMIT -> "The 64 voting NPC limit has been reached. Remove an NPC before creating another.";
};
}
if (failure instanceof java.util.concurrent.RejectedExecutionException) {
return "Voting NPC storage is busy. Wait for pending changes to finish before retrying.";
}
}
return "Voting NPC change was not confirmed. Check storage and reload before retrying; no success is assumed.";
}
private static <T> void confirm(VotingPluginMain plugin, CommandSender sender, VotingNPCManager manager, CompletionStage<T> completion,
java.util.function.Function<T, String> message) {
completion.whenComplete((value, failure) -> onSender(plugin, sender, () -> {
if (!allowed(sender)) return;
sender.sendMessage(failure == null ? message.apply(value)
: failureMessage(manager, failure));
}));
}
}
@@ -0,0 +1,29 @@
package com.bencodez.votingplugin.npc;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Pattern;
/** Durable definition; it contains no live world, player, entity or reward state. */
public record VotingNPCDefinition(String id, UUID identity, UUID world, double x, double y, double z,
float yaw, float pitch, String name) {
private static final Pattern ID = Pattern.compile("[a-z0-9][a-z0-9_-]{0,39}");
public VotingNPCDefinition {
if (id == null || !ID.matcher(id).matches()) throw new IllegalArgumentException("Invalid voting NPC ID");
Objects.requireNonNull(identity, "identity");
Objects.requireNonNull(world, "world");
if (!Double.isFinite(x) || !Double.isFinite(y) || !Double.isFinite(z)
|| Math.abs(x) > 30_000_000 || Math.abs(z) > 30_000_000
|| !Float.isFinite(yaw) || !Float.isFinite(pitch)) {
throw new IllegalArgumentException("Invalid voting NPC position");
}
if (name == null || name.isEmpty() || name.length() > 128
|| name.codePoints().anyMatch(Character::isISOControl)) {
throw new IllegalArgumentException("Invalid voting NPC name");
}
}
public int chunkX() { return Math.floorDiv((int) Math.floor(x), 16); }
public int chunkZ() { return Math.floorDiv((int) Math.floor(z), 16); }
}
@@ -0,0 +1,450 @@
package com.bencodez.votingplugin.npc;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityCombustEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityInteractEvent;
import org.bukkit.event.entity.EntityTargetEvent;
import org.bukkit.event.entity.EntityTeleportEvent;
import org.bukkit.event.entity.EntityTransformEvent;
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.vehicle.VehicleEnterEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.bukkit.inventory.EquipmentSlot;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.util.BukkitCompletionScheduler;
/** Persistent definitions, transient public villagers, and no vote/reward accounting. */
public final class VotingNPCManager implements Listener, AutoCloseable {
private final VotingPluginMain plugin;
private final VotingNPCScheduler scheduler;
private final Consumer<Player> open;
private final Map<String, RuntimeNPC> resources = new ConcurrentHashMap<>();
private final Set<String> capacityDemand = ConcurrentHashMap.newKeySet();
private final AtomicBoolean capacityWakeQueued = new AtomicBoolean();
private final Map<UUID, RuntimeNPC> entities = new ConcurrentHashMap<>();
private final Set<ChunkKey> unloadingChunks = ConcurrentHashMap.newKeySet();
private final Set<UUID> unloadingWorlds = ConcurrentHashMap.newKeySet();
private final Map<UUID, Long> recentClicks = new LinkedHashMap<>();
private volatile VotingNPCRegistry registry;
private volatile boolean enabled;
private volatile boolean closed;
private volatile long configuration;
private final AtomicBoolean warned = new AtomicBoolean();
private record ChunkKey(UUID world, int x, int z) { }
private static final class RuntimeNPC {
final VotingNPCDefinition definition;
final Location anchor;
final long configuration;
boolean closed, cleanupQueued, reconstructRequested;
Villager entity;
UUID entityId;
RuntimeNPC(VotingNPCDefinition definition, World world, long configuration) {
this.definition = definition;
this.configuration = configuration;
anchor = new Location(world, definition.x(), definition.y(), definition.z(), definition.yaw(), definition.pitch());
}
}
public VotingNPCManager(VotingPluginMain plugin) {
this(plugin, new VotingNPCScheduler(plugin), null, player -> openVotingSites(plugin, player));
}
static void openVotingSites(VotingPluginMain plugin, Player player) {
// Reuse the normal command's permissions, asynchronous user loading,
// configured GUI, cooldown display, and URL handling; no root alias.
for (var handler : plugin.getVoteCommand()) {
if (handler.runCommand(player, new String[] { "URL" })) return;
}
player.sendMessage("Voting-site selection is unavailable.");
}
VotingNPCManager(VotingPluginMain plugin, VotingNPCScheduler scheduler, VotingNPCRegistry registry, Consumer<Player> open) {
this.plugin = plugin;
this.scheduler = scheduler;
this.registry = registry;
this.open = open;
}
public static boolean supported() {
try {
Entity.class.getMethod("setPersistent", boolean.class);
Entity.class.getMethod("setGravity", boolean.class);
Entity.class.getMethod("setInvulnerable", boolean.class);
LivingEntity.class.getMethod("setAI", boolean.class);
LivingEntity.class.getMethod("setCollidable", boolean.class);
World.class.getMethod("spawn", Location.class, Class.class, Consumer.class);
return true;
} catch (ReflectiveOperationException | LinkageError unavailable) { return false; }
}
/** Configuration callers do no file I/O and do not wait for storage. */
public void configure(boolean requested) {
VotingNPCRegistry owner;
CompletionStage<List<VotingNPCDefinition>> loading;
long generation;
boolean activate;
synchronized (this) {
if (closed) return;
generation = ++configuration;
activate = requested && supported();
enabled = activate;
if (!activate) { owner = null; loading = null; }
else {
if (registry == null) {
registry = new VotingNPCRegistry(plugin.getDataFolder().toPath().resolve("VotingNPCs.json"));
loading = registry.initialLoad();
} else loading = registry.reload();
owner = registry;
}
}
if (!activate) {
for (RuntimeNPC npc : resources.values()) retire(npc);
if (requested) report(new IllegalStateException("Vanilla voting NPC API is unavailable"));
return;
}
loading.whenComplete((definitions, failure) -> {
synchronized (VotingNPCManager.this) {
if (closed || !enabled || generation != configuration || registry != owner) return;
}
if (failure != null) {
for (RuntimeNPC npc : resources.values()) retire(npc);
report(failure);
} else {
warned.set(false);
reconcile();
}
});
}
public boolean available() {
VotingNPCRegistry owner = registry;
return !closed && enabled && owner != null && owner.available();
}
public List<VotingNPCDefinition> definitions() {
VotingNPCRegistry owner = registry;
return owner == null ? List.of() : owner.snapshot();
}
/** Returns durable definition completion; runtime creation can still await a loaded chunk. */
public CompletionStage<VotingNPCDefinition> create(VotingNPCDefinition definition) {
VotingNPCRegistry owner = registry;
if (!available()) return CompletableFuture.failedFuture(new IllegalStateException("Enable VotingNPCs and load valid storage first"));
CompletionStage<VotingNPCDefinition> completion = owner.create(definition);
completion.whenComplete((value, failure) -> mutationFinished(owner, failure));
return completion;
}
public CompletionStage<Boolean> remove(String id) {
VotingNPCRegistry owner = registry;
if (!available()) return CompletableFuture.failedFuture(new IllegalStateException("Enable VotingNPCs and load valid storage first"));
CompletionStage<Boolean> completion = owner.remove(id);
completion.whenComplete((value, failure) -> mutationFinished(owner, failure));
return completion;
}
private void mutationFinished(VotingNPCRegistry owner, Throwable failure) {
if (owner != registry || closed) return;
if (failure == null) reconcile();
else if (!owner.available()) {
for (RuntimeNPC npc : resources.values()) retire(npc);
report(failure);
}
}
private boolean current(RuntimeNPC npc) {
return available() && definitions().contains(npc.definition);
}
private void reconcile() {
retryPendingCleanup();
capacityDemand.retainAll(definitions().stream().map(VotingNPCDefinition::id).toList());
List<VotingNPCDefinition> snapshot = definitions();
Set<ChunkKey> chunks = new HashSet<>();
Set<UUID> worlds = new HashSet<>();
for (var value : snapshot) { chunks.addAll(occupiedChunks(value)); worlds.add(value.world()); }
unloadingChunks.retainAll(chunks);
unloadingWorlds.retainAll(worlds);
for (RuntimeNPC npc : resources.values()) if (!current(npc)) retire(npc);
if (!available()) return;
scheduler.global(() -> {
if (!available()) return;
for (var value : definitions()) ensure(value);
}, this::report);
}
/** Same conservative half-block bounds used for creation ownership and chunk availability. */
private static Set<ChunkKey> occupiedChunks(VotingNPCDefinition value) {
Set<ChunkKey> chunks = new HashSet<>();
for (double dx : new double[] { -.5, .5 }) for (double dz : new double[] { -.5, .5 }) {
chunks.add(new ChunkKey(value.world(), (int) Math.floor(value.x() + dx) >> 4,
(int) Math.floor(value.z() + dz) >> 4));
}
return chunks;
}
private boolean unloading(VotingNPCDefinition value) {
return occupiedChunks(value).stream().anyMatch(unloadingChunks::contains);
}
private void ensure(VotingNPCDefinition value) {
if (!available() || !definitions().contains(value) || unloadingWorlds.contains(value.world()) || unloading(value)) return;
World world = Bukkit.getWorld(value.world());
if (world == null) return;
RuntimeNPC npc = new RuntimeNPC(value, world, configuration);
RuntimeNPC previous;
synchronized (resources) {
previous = resources.get(value.id());
if (previous == null) {
if (resources.size() >= VotingNPCStore.MAX_NPCS) {
if (capacityDemand.size() < VotingNPCStore.MAX_NPCS) capacityDemand.add(value.id());
return;
}
resources.put(value.id(), npc);
capacityDemand.remove(value.id());
}
}
if (previous != null) {
synchronized (previous) { if (previous.closed) previous.reconstructRequested = true; }
return;
}
scheduler.region(npc.anchor, () -> spawn(npc), failure -> { retire(npc); report(failure); });
}
private boolean loadedArea(RuntimeNPC npc) {
World world = npc.anchor.getWorld();
if (Bukkit.getWorld(npc.definition.world()) != world || unloadingWorlds.contains(npc.definition.world())
|| unloading(npc.definition)) return false;
for (double dx : new double[] { -.5, .5 }) for (double dz : new double[] { -.5, .5 }) {
Location edge = npc.anchor.clone().add(dx, 0, dz);
if (!scheduler.owns(edge) || !world.isChunkLoaded(edge.getBlockX() >> 4, edge.getBlockZ() >> 4)) return false;
}
return true;
}
private void spawn(RuntimeNPC npc) {
try {
synchronized (npc) {
if (npc.closed || !current(npc) || !loadedArea(npc)) {
boolean capacityReleased = !npc.closed && !current(npc);
forget(npc, capacityReleased);
return;
}
if (npc.entity != null) return;
Villager spawned = npc.anchor.getWorld().spawn(npc.anchor, Villager.class, villager -> {
npc.entity = villager;
npc.entityId = villager.getUniqueId();
entities.put(npc.entityId, npc);
villager.setPersistent(false);
villager.setAI(false);
villager.setGravity(false);
villager.setInvulnerable(true);
villager.setSilent(true);
villager.setCollidable(false);
villager.setCanPickupItems(false);
villager.setRemoveWhenFarAway(false);
villager.setCustomName(ChatColor.translateAlternateColorCodes('&', npc.definition.name()));
villager.setCustomNameVisible(true);
});
if (spawned != npc.entity || !spawned.isValid() || !current(npc) || npc.closed) {
throw new IllegalStateException("Voting NPC spawn was cancelled or superseded");
}
}
} catch (RuntimeException | LinkageError failure) { retire(npc); report(failure); }
}
private void retire(RuntimeNPC npc) {
Villager entity;
synchronized (npc) {
npc.closed = true;
entity = npc.entity;
if (entity == null) { forget(npc, false); return; }
if (npc.cleanupQueued) return;
npc.cleanupQueued = true;
}
scheduler.entity(entity, () -> {
try { entity.remove(); forget(npc); }
catch (RuntimeException | LinkageError failure) { cleanupFailed(npc, failure); }
}, () -> forget(npc), failure -> cleanupFailed(npc, failure));
}
private void cleanupFailed(RuntimeNPC npc, Throwable failure) {
synchronized (npc) {
npc.cleanupQueued = false;
npc.reconstructRequested = true;
}
// Keep the exact identity and capacity reservation. Rejected scheduling
// is not successful removal and must not permit duplicate reconstruction.
report(failure);
}
private void retryPendingCleanup() {
for (RuntimeNPC npc : resources.values()) {
synchronized (npc) {
if (!npc.closed || !npc.reconstructRequested || npc.cleanupQueued || npc.entity == null) continue;
}
retire(npc);
}
}
private void forget(RuntimeNPC npc) { forget(npc, false); }
private void forget(RuntimeNPC npc, boolean capacityReleased) {
boolean reconstruct, removed;
synchronized (npc) {
reconstruct = npc.entity != null && (npc.reconstructRequested || npc.configuration != configuration
|| definitions().stream().anyMatch(value -> value.id().equals(npc.definition.id()) && !value.equals(npc.definition)));
if (npc.entityId != null) entities.remove(npc.entityId, npc);
npc.entity = null;
removed = resources.remove(npc.definition.id(), npc);
}
// A successor sharing this configured ID must wait for exact predecessor
// cleanup. Missing/unloaded spawn attempts do not recursively retry.
if (reconstruct && available()) scheduler.global(() -> {
for (var value : definitions()) if (value.id().equals(npc.definition.id())) ensure(value);
}, this::report);
if (removed && !capacityDemand.isEmpty() && available()
&& capacityWakeQueued.compareAndSet(false, true)) {
scheduler.global(() -> {
capacityWakeQueued.set(false);
for (var value : definitions()) if (capacityDemand.contains(value.id())) ensure(value);
}, failure -> {
capacityWakeQueued.set(false);
report(failure);
});
}
}
private void report(Throwable failure) {
if (warned.compareAndSet(false, true)) {
try {
plugin.getLogger().warning("Voting NPCs could not complete an operation; definitions are retained. Check API support and VotingNPCs.json, then reload. Cause: "
+ failure.getClass().getSimpleName());
} catch (RuntimeException ignored) { /* Reporting must not prevent independent cleanup. */ }
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void interact(PlayerInteractEntityEvent event) {
RuntimeNPC npc = entities.get(event.getRightClicked().getUniqueId());
if (npc == null) return;
boolean cancelled = event.isCancelled();
event.setCancelled(true); // Only this exact owned entity; never trading.
if (cancelled || event.getHand() != EquipmentSlot.HAND || !current(npc)) return;
synchronized (npc) { if (npc.closed) { retire(npc); return; } }
Player player = event.getPlayer();
long now = System.nanoTime();
synchronized (recentClicks) {
recentClicks.entrySet().removeIf(entry -> now - entry.getValue() >= 300_000_000L);
if (recentClicks.containsKey(player.getUniqueId()) || recentClicks.size() >= 256) return;
recentClicks.put(player.getUniqueId(), now);
}
BukkitCompletionScheduler.run(plugin, player, () -> {
if (!player.isOnline() || !current(npc) || !player.getWorld().getUID().equals(npc.definition.world())
|| player.getLocation().distanceSquared(npc.anchor) > 36) return;
synchronized (npc) { if (npc.closed) return; }
open.accept(player);
}, () -> { }, () -> { });
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void interactAt(PlayerInteractAtEntityEvent event) { interact(event); }
private boolean owned(Entity entity) { return entity != null && entities.containsKey(entity.getUniqueId()); }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void damage(EntityDamageEvent event) { if (owned(event.getEntity())) event.setCancelled(true); }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void combust(EntityCombustEvent event) { if (owned(event.getEntity())) event.setCancelled(true); }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void teleport(EntityTeleportEvent event) { if (owned(event.getEntity())) event.setCancelled(true); }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void transform(EntityTransformEvent event) { if (owned(event.getEntity())) event.setCancelled(true); }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void target(EntityTargetEvent event) { if (owned(event.getEntity()) || owned(event.getTarget())) event.setCancelled(true); }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void blockInteract(EntityInteractEvent event) { if (owned(event.getEntity())) event.setCancelled(true); }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void vehicle(VehicleEnterEvent event) { if (owned(event.getEntered())) event.setCancelled(true); }
@EventHandler public void quit(PlayerQuitEvent event) { synchronized (recentClicks) { recentClicks.remove(event.getPlayer().getUniqueId()); } }
@EventHandler(priority = EventPriority.MONITOR)
public void chunkLoad(ChunkLoadEvent event) {
UUID world = event.getWorld().getUID();
unloadingChunks.remove(new ChunkKey(world, event.getChunk().getX(), event.getChunk().getZ()));
retryPendingCleanup();
ChunkKey chunk = new ChunkKey(world, event.getChunk().getX(), event.getChunk().getZ());
for (var value : definitions()) if (occupiedChunks(value).contains(chunk)) ensure(value);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void chunkUnload(ChunkUnloadEvent event) {
ChunkKey chunk = new ChunkKey(event.getWorld().getUID(), event.getChunk().getX(), event.getChunk().getZ());
if (definitions().stream().noneMatch(value -> occupiedChunks(value).contains(chunk))) return;
unloadingChunks.add(chunk);
for (RuntimeNPC npc : resources.values()) if (occupiedChunks(npc.definition).contains(chunk)) retire(npc);
}
@EventHandler(priority = EventPriority.MONITOR)
public void worldLoad(WorldLoadEvent event) {
UUID world = event.getWorld().getUID();
if (definitions().stream().noneMatch(value -> value.world().equals(world))) return;
unloadingWorlds.remove(world);
reconcile();
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void worldUnload(WorldUnloadEvent event) {
UUID world = event.getWorld().getUID();
if (definitions().stream().noneMatch(value -> value.world().equals(world))) return;
unloadingWorlds.add(world);
for (RuntimeNPC npc : resources.values()) if (npc.definition.world().equals(world)) retire(npc);
}
public int runtimeCount() { return resources.size(); }
public String runtimeState(String id) {
RuntimeNPC npc = resources.get(id);
if (npc == null) return "waiting for a loaded, available area";
synchronized (npc) {
if (npc.closed) return "cleanup pending";
return npc.entity == null ? "creation scheduled" : "villager created";
}
}
@Override public void close() {
synchronized (this) { if (closed) return; closed = true; enabled = false; ++configuration; }
VotingNPCRegistry owner = registry;
if (owner != null) owner.close();
for (RuntimeNPC npc : new ArrayList<>(resources.values())) retire(npc);
synchronized (recentClicks) { recentClicks.clear(); }
}
}
@@ -0,0 +1,206 @@
package com.bencodez.votingplugin.npc;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
/**
* One bounded, daemon file owner. Completion means the definition was persisted,
* not that a world entity has spawned. Callbacks have no player/region ownership.
*/
final class VotingNPCRegistry implements AutoCloseable {
enum Rejection { DUPLICATE_ID, DUPLICATE_IDENTITY, LIMIT }
static final class ValidationFailure extends IllegalArgumentException {
private static final long serialVersionUID = 1L;
final Rejection reason;
ValidationFailure(Rejection reason) { super("Voting NPC request rejected"); this.reason = reason; }
}
private final Object admission = new Object();
private final ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(128);
private final CountDownLatch stopped = new CountDownLatch(1);
private final VotingNPCStore store;
private final Path lockPath;
private final CompletableFuture<List<VotingNPCDefinition>> initial;
private volatile Map<String, VotingNPCDefinition> definitions = Map.of();
private volatile boolean available;
private volatile boolean closed;
// Only the file worker accesses these resources.
private FileChannel lockChannel;
private FileLock lease;
private boolean storageReady;
VotingNPCRegistry(Path file) {
this(file, new VotingNPCStore(file));
}
VotingNPCRegistry(Path file, VotingNPCStore store) {
this.store = store;
lockPath = file.toAbsolutePath().normalize().resolveSibling(file.getFileName() + ".lock");
initial = submit(false, this::readDefinitions);
Thread thread = new Thread(this::work, "VotingPlugin-NPC-storage");
thread.setDaemon(true);
try { thread.start(); }
catch (RuntimeException | Error failure) {
closed = true;
initial.completeExceptionally(failure);
stopped.countDown();
throw failure;
}
}
CompletionStage<List<VotingNPCDefinition>> initialLoad() { return initial; }
boolean available() { return !closed && available; }
List<VotingNPCDefinition> snapshot() { return List.copyOf(definitions.values()); }
CompletionStage<List<VotingNPCDefinition>> reload() {
synchronized (admission) {
if (closed) return rejected("Voting NPC registry is closed");
var completion = submit(false, this::readDefinitions);
// A rejected reload changes nothing. The worker publishes readiness
// under this same short lock, after admission has finished.
if (!completion.isCompletedExceptionally()) available = false;
return completion;
}
}
CompletionStage<VotingNPCDefinition> create(VotingNPCDefinition definition) {
return submit(true, () -> {
if (definition == null) throw new IllegalArgumentException("Voting NPC definition is required");
if (definitions.containsKey(definition.id())) throw new ValidationFailure(Rejection.DUPLICATE_ID);
if (definitions.values().stream().anyMatch(value -> value.identity().equals(definition.identity()))) {
throw new ValidationFailure(Rejection.DUPLICATE_IDENTITY);
}
if (definitions.size() >= VotingNPCStore.MAX_NPCS) throw new ValidationFailure(Rejection.LIMIT);
var next = new LinkedHashMap<>(definitions);
next.put(definition.id(), definition);
publish(next);
return definition;
});
}
CompletionStage<Boolean> remove(String id) {
return submit(true, () -> {
if (!definitions.containsKey(id)) return false;
var next = new LinkedHashMap<>(definitions);
next.remove(id);
publish(next);
return true;
});
}
private List<VotingNPCDefinition> readDefinitions() throws IOException {
try {
ownFile();
List<VotingNPCDefinition> loaded = store.load();
var next = new LinkedHashMap<String, VotingNPCDefinition>();
for (VotingNPCDefinition value : loaded) next.put(value.id(), value);
definitions = java.util.Collections.unmodifiableMap(next);
storageReady = true;
synchronized (admission) { available = true; }
return loaded;
} catch (IOException | RuntimeException failure) {
storageReady = false;
synchronized (admission) { available = false; }
throw failure;
}
}
private void publish(LinkedHashMap<String, VotingNPCDefinition> next) throws IOException {
// The worker holds the process-independent lease through shutdown drain.
try { store.save(List.copyOf(next.values())); }
catch (IOException | RuntimeException | Error failure) {
// A failed write may already be visible. Neither later admitted
// mutations nor a new request may overwrite it from the old map.
storageReady = false;
synchronized (admission) { available = false; }
throw failure;
}
definitions = java.util.Collections.unmodifiableMap(next);
}
private <T> CompletableFuture<T> submit(boolean needsReady, Callable<T> action) {
synchronized (admission) {
if (closed || (needsReady && !available)) return rejected("Voting NPC storage is not available");
var completion = new CompletableFuture<T>();
Runnable task = () -> {
try {
if (needsReady && !storageReady) throw new RejectedExecutionException("Reload valid voting NPC storage before editing");
completion.complete(action.call());
}
catch (Throwable failure) { completion.completeExceptionally(failure); }
};
if (!queue.offer(task)) completion.completeExceptionally(new RejectedExecutionException("Voting NPC storage queue is full"));
return completion;
}
}
private static <T> CompletableFuture<T> rejected(String message) {
return CompletableFuture.failedFuture(new RejectedExecutionException(message));
}
private void ownFile() throws IOException {
if (lease != null && lease.isValid()) return;
if (!Files.isDirectory(lockPath.getParent().toRealPath(), LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Voting NPC data directory is unavailable");
}
FileChannel candidate = FileChannel.open(lockPath, StandardOpenOption.CREATE,
StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS);
try {
FileLock acquired;
try { acquired = candidate.tryLock(); }
catch (OverlappingFileLockException overlap) { throw new IOException("Voting NPC file still has an active owner", overlap); }
if (acquired == null) throw new IOException("Voting NPC file still has an active owner");
lockChannel = candidate;
lease = acquired;
} finally {
if (lockChannel != candidate) candidate.close();
}
}
private void work() {
try {
while (!closed || !queue.isEmpty()) {
Runnable task;
try { task = queue.take(); }
catch (InterruptedException interrupted) { continue; }
task.run();
}
} finally {
available = false;
try {
if (lockChannel != null) lockChannel.close();
} catch (IOException ignored) {
// Closing this channel releases its OS lease; never do this I/O
// under an admission or executor lock on a gameplay caller.
} finally { stopped.countDown(); }
}
}
/** Stops admission without blocking or interrupting an accepted file write. */
@Override public void close() {
synchronized (admission) {
if (closed) return;
closed = true;
queue.offer(() -> { }); // Wake an idle worker; a full queue already wakes it.
}
}
/** Test/owner-worker observation only; gameplay callers must never wait here. */
boolean awaitClosed(long time, TimeUnit unit) throws InterruptedException { return stopped.await(time, unit); }
}
@@ -0,0 +1,77 @@
package com.bencodez.votingplugin.npc;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import com.bencodez.votingplugin.VotingPluginMain;
import com.bencodez.votingplugin.util.BukkitCompletionScheduler;
/** World creation uses a region; subsequent cleanup follows the actual entity. */
class VotingNPCScheduler {
private final VotingPluginMain plugin;
private final Method locationOwner;
private final Method entityOwner;
VotingNPCScheduler(VotingPluginMain plugin) {
this.plugin = plugin;
Method location = null, entity = null;
try {
Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
location = Bukkit.class.getMethod("isOwnedByCurrentRegion", Location.class);
entity = Bukkit.class.getMethod("isOwnedByCurrentRegion", Entity.class);
} catch (ClassNotFoundException ordinaryServer) {
// Spigot/Paper have a single world-owner thread.
} catch (ReflectiveOperationException unavailable) {
throw new IllegalStateException("Voting NPC region ownership API is unavailable", unavailable);
}
locationOwner = location;
entityOwner = entity;
}
boolean owns(Location location) { return owns(locationOwner, location); }
boolean owns(Entity entity) { return owns(entityOwner, entity); }
private boolean owns(Method method, Object value) {
if (method == null) return Bukkit.isPrimaryThread();
try { return Boolean.TRUE.equals(method.invoke(null, value)); }
catch (ReflectiveOperationException failure) { throw new IllegalStateException("Cannot verify voting NPC ownership", failure); }
}
void global(Runnable action, Consumer<Throwable> failure) {
try {
plugin.getBukkitScheduler().getFoliaLib().getImpl().runNextTick(ignored -> action.run())
.whenComplete((ignored, failed) -> { if (failed != null) failure.accept(failed); });
} catch (RuntimeException | LinkageError rejected) { failure.accept(rejected); }
}
void region(Location anchor, Runnable action, Consumer<Throwable> failure) {
try {
if (owns(anchor)) action.run();
else plugin.getBukkitScheduler().getFoliaLib().getImpl().runAtLocation(anchor, ignored -> action.run())
.whenComplete((ignored, failed) -> { if (failed != null) failure.accept(failed); });
} catch (RuntimeException | LinkageError rejected) { failure.accept(rejected); }
}
void entity(Entity entity, Runnable action, Runnable retired, Consumer<Throwable> failure) {
AtomicBoolean executed = new AtomicBoolean();
Runnable run = () -> { if (executed.compareAndSet(false, true)) action.run(); };
Runnable gone = () -> { if (executed.compareAndSet(false, true)) retired.run(); };
try {
if (owns(entity)) { run.run(); return; }
plugin.getBukkitScheduler().getFoliaLib().getImpl().runAtEntityWithFallback(entity,
ignored -> run.run(), gone).whenComplete((status, failed) -> {
if (failed != null) failure.accept(failed);
else if (status instanceof Enum<?> result && "ENTITY_RETIRED".equals(result.name())) gone.run();
else if (!BukkitCompletionScheduler.isSuccessfulEntityTaskResult(status)) {
// Scheduler retirement does not prove the entity was removed.
failure.accept(new IllegalStateException("Voting NPC entity scheduler is unavailable"));
}
});
} catch (RuntimeException | LinkageError rejected) { failure.accept(rejected); }
}
}
@@ -0,0 +1,199 @@
package com.bencodez.votingplugin.npc;
import java.io.IOException;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
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.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import com.bencodez.votingplugin.util.DurableFiles;
/** Single storage-worker owner. Never call this file boundary on a gameplay thread. */
class VotingNPCStore {
static final int MAX_NPCS = 64;
private static final int MAX_BYTES = 128 * 1024;
private static final Set<String> FIELDS = Set.of("id", "identity", "world", "x", "y", "z", "yaw", "pitch", "name");
private final Path file;
private boolean initialized;
private boolean uncertain;
VotingNPCStore(Path file) { this.file = file.toAbsolutePath().normalize(); }
synchronized List<VotingNPCDefinition> load() throws IOException {
initialized = false;
checkParent();
BasicFileAttributes attributes;
try { attributes = Files.readAttributes(file, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); }
catch (NoSuchFileException missing) {
if (uncertain) throw new IOException("Uncertain voting NPC definitions are missing", missing);
initialized = true;
return List.of();
}
if (!attributes.isRegularFile() || attributes.isSymbolicLink() || attributes.size() > MAX_BYTES) {
throw new IOException("Voting NPC definitions are not a bounded regular file");
}
byte[] bytes;
try (var input = Files.newInputStream(file, LinkOption.NOFOLLOW_LINKS)) {
bytes = input.readNBytes(MAX_BYTES + 1);
}
if (bytes.length > MAX_BYTES) throw new IOException("Voting NPC definitions exceed limit");
String text;
try { text = StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT).decode(ByteBuffer.wrap(bytes)).toString(); }
catch (CharacterCodingException invalid) { throw new IOException("Invalid voting NPC encoding", invalid); }
List<VotingNPCDefinition> result = decode(text);
uncertain = false;
initialized = true;
return result;
}
synchronized void save(List<VotingNPCDefinition> definitions) throws IOException {
if (!initialized || uncertain) throw new IOException("Load valid voting NPC definitions before editing");
validate(definitions);
byte[] bytes = encode(definitions).getBytes(StandardCharsets.UTF_8);
if (bytes.length > MAX_BYTES) throw new IOException("Voting NPC definitions exceed limit");
checkParent();
if (Files.isSymbolicLink(file)) throw new IOException("Voting NPC definitions cannot be a symbolic link");
Path staged = Files.createTempFile(file.getParent(), ".voting-npcs-", ".tmp");
boolean published = false;
Throwable primary = null;
try {
try (FileChannel output = FileChannel.open(staged, StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.wrap(bytes);
while (buffer.hasRemaining()) output.write(buffer);
output.force(true);
}
Files.move(staged, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
published = true;
forceDirectory(file.getParent());
} catch (IOException | RuntimeException | Error failure) {
// A visible replacement followed by a failed directory sync is not a
// rollback. Fence later mutations rather than overwrite it from old state.
if (published) uncertain = true;
primary = failure;
throw failure;
} finally {
try { Files.deleteIfExists(staged); }
catch (IOException cleanupFailure) {
if (primary != null) primary.addSuppressed(cleanupFailure);
else {
if (published) uncertain = true;
throw cleanupFailure;
}
}
}
}
private void checkParent() throws IOException {
// Operators may place the data directory below a directory alias. Only
// the definitions and lease files themselves must remain non-links.
if (!Files.isDirectory(file.getParent().toRealPath(), LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Voting NPC data directory is unavailable");
}
}
/** Kept as a narrow I/O seam so failure-after-publication can be tested. */
void forceDirectory(Path directory) throws IOException {
DurableFiles.forceDirectory(directory);
}
private static void validate(List<VotingNPCDefinition> values) throws IOException {
if (values == null || values.size() > MAX_NPCS) throw new IOException("Voting NPC definition limit exceeded");
Set<String> ids = new HashSet<>();
Set<UUID> identities = new HashSet<>();
for (VotingNPCDefinition value : values) {
if (value == null || !ids.add(value.id()) || !identities.add(value.identity())) {
throw new IOException("Duplicate or missing voting NPC identity");
}
}
}
private static String encode(List<VotingNPCDefinition> values) throws IOException {
var text = new java.io.StringWriter();
try (JsonWriter writer = new JsonWriter(text)) {
writer.setIndent(" ");
writer.beginObject().name("version").value(1).name("npcs").beginArray();
for (VotingNPCDefinition value : values) {
writer.beginObject().name("id").value(value.id()).name("identity").value(value.identity().toString())
.name("world").value(value.world().toString()).name("x").value(value.x()).name("y").value(value.y())
.name("z").value(value.z()).name("yaw").value(value.yaw()).name("pitch").value(value.pitch())
.name("name").value(value.name()).endObject();
}
writer.endArray().endObject();
}
return text.toString() + "\n";
}
private static List<VotingNPCDefinition> decode(String text) throws IOException {
try (JsonReader reader = new JsonReader(new StringReader(text))) {
Set<String> keys = new HashSet<>();
List<VotingNPCDefinition> values = null;
reader.beginObject();
while (reader.hasNext()) {
String key = reader.nextName();
if (!keys.add(key)) throw new IOException("Duplicate voting NPC document field");
if (key.equals("version")) {
if (reader.peek() != JsonToken.NUMBER || !reader.nextString().equals("1")) {
throw new IOException("Unsupported voting NPC document version");
}
} else if (key.equals("npcs")) {
values = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
if (values.size() >= MAX_NPCS) throw new IOException("Voting NPC definition limit exceeded");
values.add(readDefinition(reader));
}
reader.endArray();
} else throw new IOException("Unknown voting NPC document field");
}
reader.endObject();
if (!keys.equals(Set.of("version", "npcs")) || reader.peek() != JsonToken.END_DOCUMENT) {
throw new IOException("Incomplete or trailing voting NPC document");
}
validate(values);
return List.copyOf(values);
} catch (IllegalArgumentException | IllegalStateException invalid) {
throw new IOException("Invalid voting NPC definitions", invalid);
}
}
private static VotingNPCDefinition readDefinition(JsonReader reader) throws IOException {
var fields = new java.util.HashMap<String, String>();
reader.beginObject();
while (reader.hasNext()) {
String field = reader.nextName();
if (!FIELDS.contains(field) || fields.containsKey(field)) throw new IOException("Unknown or duplicate voting NPC field");
boolean numeric = Set.of("x", "y", "z", "yaw", "pitch").contains(field);
if (reader.peek() != (numeric ? JsonToken.NUMBER : JsonToken.STRING)) throw new IOException("Invalid voting NPC field type");
fields.put(field, reader.nextString());
}
reader.endObject();
if (!fields.keySet().equals(FIELDS)) throw new IOException("Missing voting NPC field");
return new VotingNPCDefinition(fields.get("id"), uuid(fields.get("identity")), uuid(fields.get("world")),
Double.parseDouble(fields.get("x")), Double.parseDouble(fields.get("y")), Double.parseDouble(fields.get("z")),
Float.parseFloat(fields.get("yaw")), Float.parseFloat(fields.get("pitch")), fields.get("name"));
}
private static UUID uuid(String text) {
UUID value = UUID.fromString(text);
if (!value.toString().equals(text)) throw new IllegalArgumentException("Noncanonical voting NPC identity");
return value;
}
}
@@ -40,7 +40,6 @@ 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;
@@ -57,26 +56,6 @@ 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() {
@@ -1,20 +0,0 @@
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);
}
}
});
}
}
@@ -1,277 +0,0 @@
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(); }
}
@@ -1,37 +0,0 @@
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"; };
}
}
+6 -9
View File
@@ -188,7 +188,6 @@ 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
@@ -212,14 +211,6 @@ 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
@@ -1265,3 +1256,9 @@ DisableUpdateChecking: false
# Disable checking permissions on tab complete
# Could increase performance slightly, but probably not
DisableAdvancedTab: false
# Optional public vanilla villagers opening the existing voting-site menu.
# Enable and /av reload, then /av npc create <id> [name]. No Citizens required.
# Definitions live in VotingNPCs.json; disabling keeps them but removes live NPCs.
VotingNPCs:
Enabled: false
-4
View File
@@ -294,10 +294,6 @@ 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
@@ -6,7 +6,7 @@ license="CC BY 3.0"
modId="votingplugin"
version="${project.version}"
displayName="VotingPlugin"
description="Experimental VotingPlugin NeoForge support for authenticated proxy SOCKETS votes and limited reward replay."
description="VotingPlugin NeoForge bootstrap (vote processing is not available yet)."
[[dependencies.votingplugin]]
modId="neoforge"
+5 -3
View File
@@ -170,6 +170,8 @@ permissions:
description: Allows mod commands
VotingPlugin.Admin:
description: Allows admincommands
children:
VotingPlugin.Commands.AdminVote: true
VotingPlugin.TopVoter.Ignore:
description: Allows you to be exempt from top voter
VotingPlugin.NoRemind:
@@ -196,8 +198,8 @@ permissions:
description: Remind if player can vote on all votesites, rather than any
default: false
VotingPlugin.Commands.Vote.Streaks:
description: View your read-only vote streak progression
VotingPlugin.Commands.AdminVote.NPC:
description: Create, inspect, list and remove saved vanilla voting NPCs
default: op
children:
VotingPlugin.Commands.Vote: true
VotingPlugin.Commands.AdminVote: true
@@ -1,7 +1,4 @@
package com.bencodez.votingplugin.tests;
import com.bencodez.votingplugin.VotingPluginHooks;
import com.bencodez.votingplugin.VotingPluginMain;
package com.bencodez.votingplugin;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -1,8 +1,4 @@
package com.bencodez.votingplugin.tests;
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
import com.bencodez.votingplugin.VotingPluginMain;
package com.bencodez.votingplugin;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -48,7 +44,7 @@ class VotingPluginMainBackendProxyPublicationTest {
setField(plugin, "bungeeSettings", settings);
when(settings.isUseBungeecoord()).thenReturn(true);
VotingPluginMainAccess.reloadBackendProxyRuntime(plugin, true, false);
plugin.reloadBackendProxyRuntime(true, false);
verify(active).reloadSharedTransportSecurity();
verify(active).reloadPresenceReporting();
@@ -67,7 +63,7 @@ class VotingPluginMainBackendProxyPublicationTest {
// Full-editor Control preparation reloads settings before this replacement
// can be validated or published. It must not reconfigure the live handler.
VotingPluginMainAccess.reloadBackendProxyRuntime(plugin, false, false);
plugin.reloadBackendProxyRuntime(false, false);
VotingPluginMain.BackendProxyRestart restart = restart(previous, replacement);
assertThrows(IllegalStateException.class, () -> plugin.completeBackendProxyHandlerRestart(restart));
@@ -83,7 +79,7 @@ class VotingPluginMainBackendProxyPublicationTest {
setField(plugin, "bungeeSettings", settings);
when(settings.isUseBungeecoord()).thenReturn(true);
VotingPluginMainAccess.reloadBackendProxyRuntime(plugin, false, false);
plugin.reloadBackendProxyRuntime(false, false);
assertNull(plugin.getBackendProxyHandler());
}
@@ -100,7 +96,7 @@ class VotingPluginMainBackendProxyPublicationTest {
Method load = VotingPluginMain.class.getDeclaredMethod("loadBungeeHandler");
load.setAccessible(true);
assertDoesNotThrow(() -> ReflectiveAccess.invoke(load, plugin, new Object[]{}));
assertDoesNotThrow(() -> load.invoke(plugin));
assertNull(plugin.getBackendProxyHandler());
}
@@ -161,7 +157,7 @@ class VotingPluginMainBackendProxyPublicationTest {
return null;
}).when(replacement).activatePresenceReporting();
VotingPluginMainAccess.publishBackendProxyHandler(plugin, null, replacement);
plugin.publishBackendProxyHandler(null, replacement);
assertSame(replacement, plugin.getBackendProxyHandler());
}
@@ -175,7 +171,7 @@ class VotingPluginMainBackendProxyPublicationTest {
.when(replacement).activatePresenceReporting();
org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException.class,
() -> VotingPluginMainAccess.publishBackendProxyHandler(plugin, previous, replacement));
() -> plugin.publishBackendProxyHandler(previous, replacement));
assertSame(previous, plugin.getBackendProxyHandler());
verify(replacement).abortStagedInboundTo(previous);
@@ -272,7 +268,7 @@ class VotingPluginMainBackendProxyPublicationTest {
org.mockito.ArgumentMatchers.anyLong());
verify(previous, never()).close();
VotingPluginMainAccess.closePublishedPreviousBackendProxyHandler(plugin, previous, "after publication");
plugin.closePublishedPreviousBackendProxyHandler(previous, "after publication");
assertTrue(closed.await(1, TimeUnit.SECONDS));
assertTrue(closeThread.get().startsWith("VotingPlugin-Retired-Backend"));
}
@@ -336,7 +332,7 @@ class VotingPluginMainBackendProxyPublicationTest {
try {
org.junit.jupiter.api.Assertions.assertTimeoutPreemptively(java.time.Duration.ofMillis(250),
() -> VotingPluginMainAccess.closePublishedPreviousBackendProxyHandler(plugin, previous, "after publication"));
() -> plugin.closePublishedPreviousBackendProxyHandler(previous, "after publication"));
assertTrue(closeStarted.await(1, TimeUnit.SECONDS));
assertTrue(closeThread.get().startsWith("VotingPlugin-Retired-Backend"));
} finally {
@@ -709,7 +705,7 @@ class VotingPluginMainBackendProxyPublicationTest {
Constructor<VotingPluginMain.BackendProxyRestart> constructor = VotingPluginMain.BackendProxyRestart.class
.getDeclaredConstructor(BackendProxyHandler.class, BackendProxyHandler.class, boolean.class, boolean.class);
constructor.setAccessible(true);
return ReflectiveAccess.newInstance(constructor, new Object[]{previous, replacement, replacement == null, previousRequiresPreparation});
return constructor.newInstance(previous, replacement, replacement == null, previousRequiresPreparation);
}
private void setBackendProxyHandler(VotingPluginMain plugin, BackendProxyHandler handler) throws Exception {
@@ -719,13 +715,13 @@ class VotingPluginMainBackendProxyPublicationTest {
private void setField(VotingPluginMain plugin, String name, Object value) throws Exception {
Field field = VotingPluginMain.class.getDeclaredField(name);
field.setAccessible(true);
ReflectiveAccess.set(field, plugin, value);
field.set(plugin, value);
}
private void setRestartField(VotingPluginMain.BackendProxyRestart restart, String name, Object value)
throws Exception {
Field field = VotingPluginMain.BackendProxyRestart.class.getDeclaredField(name);
field.setAccessible(true);
ReflectiveAccess.set(field, restart, value);
field.set(restart, value);
}
}
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests;
import com.bencodez.votingplugin.VotingPluginMain;
package com.bencodez.votingplugin;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
@@ -158,7 +156,7 @@ class VotingPluginMainPlaceholderPresenceTest {
try (var uuidLookup = mockStatic(UuidLookup.class)) {
uuidLookup.when(UuidLookup::getInstance).thenReturn(lookup);
assertEquals(storageUuid, VotingPluginMainAccess.placeholderStorageUuid(player, false));
assertEquals(storageUuid, VotingPluginMain.placeholderStorageUuid(player, false));
}
}
@@ -173,7 +171,7 @@ class VotingPluginMainPlaceholderPresenceTest {
try (var uuidLookup = mockStatic(UuidLookup.class)) {
uuidLookup.when(UuidLookup::getInstance).thenReturn(lookup);
assertEquals(bukkitUuid, VotingPluginMainAccess.placeholderStorageUuid(player, false));
assertEquals(bukkitUuid, VotingPluginMain.placeholderStorageUuid(player, false));
}
}
@@ -186,7 +184,7 @@ class VotingPluginMainPlaceholderPresenceTest {
try (var uuidLookup = mockStatic(UuidLookup.class)) {
uuidLookup.when(UuidLookup::getInstance).thenReturn(lookup);
assertEquals(bukkitUuid, VotingPluginMainAccess.placeholderStorageUuid(player, true));
assertEquals(bukkitUuid, VotingPluginMain.placeholderStorageUuid(player, true));
}
}
}
@@ -1,10 +1,4 @@
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;
package com.bencodez.votingplugin.backendproxy;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -203,7 +197,7 @@ class BackendProxyHandlerLifecycleTest {
TransportEnvelopeEncryption.Domain.PROXY_BACKEND, false));
BackendProxyTransportManager manager = (BackendProxyTransportManager) getField(handler, "transportManager");
BackendProxyTransport transport = method == BungeeMethod.REDIS
? new RedisBackendProxyTransport(plugin, ProcessedVoteCacheAccess.create())
? new RedisBackendProxyTransport(plugin, new ProcessedVoteCache())
: new MqttBackendProxyTransport(plugin);
setField(transport, "authenticator", SharedTransportEnvelopeAuthenticator.load(keyFile,
Mode.COMPATIBILITY));
@@ -239,7 +233,7 @@ class BackendProxyHandlerLifecycleTest {
setField(handler, "method", BungeeMethod.REDIS);
setField(handler, "sharedTransportMode", Mode.COMPATIBILITY);
setField(handler, "communicationEncryptionEnabled", false);
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(plugin, ProcessedVoteCacheAccess.create());
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(plugin, new ProcessedVoteCache());
SharedTransportEnvelopeAuthenticator original = SharedTransportEnvelopeAuthenticator.load(
dataDirectory.resolve("secretkey.key"), Mode.COMPATIBILITY);
setField(transport, "authenticator", original);
@@ -273,7 +267,7 @@ class BackendProxyHandlerLifecycleTest {
setField(handler, "communicationEncryption", originalEncryption);
BackendProxyTransportManager manager = (BackendProxyTransportManager) getField(handler, "transportManager");
BackendProxyTransport transport = method == BungeeMethod.REDIS
? new RedisBackendProxyTransport(plugin, ProcessedVoteCacheAccess.create())
? new RedisBackendProxyTransport(plugin, new ProcessedVoteCache())
: new MqttBackendProxyTransport(plugin);
SharedTransportEnvelopeAuthenticator originalAuthenticator =
SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED);
@@ -304,7 +298,7 @@ class BackendProxyHandlerLifecycleTest {
handler.activateInboundMessages();
Runnable localDispatch = mock(Runnable.class);
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler,
handler.dispatchIncomingAfterPublication(
JsonEnvelope.builder(VotingPluginWire.SUB_BUNGEE_TIME_CHANGE).build(), localDispatch);
verify(scheduler).runTaskAsynchronously(plugin, localDispatch);
@@ -344,9 +338,9 @@ class BackendProxyHandlerLifecycleTest {
return null;
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, voteEnvelope, mock(Runnable.class));
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, updateEnvelope, mock(Runnable.class));
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, onlineEnvelope, mock(Runnable.class));
handler.dispatchIncomingAfterPublication(voteEnvelope, mock(Runnable.class));
handler.dispatchIncomingAfterPublication(updateEnvelope, mock(Runnable.class));
handler.dispatchIncomingAfterPublication(onlineEnvelope, mock(Runnable.class));
assertEquals(1, asyncTasks.size(), "only the head of the ordered vote lane should be scheduled");
@@ -397,9 +391,9 @@ class BackendProxyHandlerLifecycleTest {
return null;
}).when(router).handleOrderedVote(eq(release), any());
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, blockedVote, mock(Runnable.class));
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, release, mock(Runnable.class));
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, secondRelease, mock(Runnable.class));
handler.dispatchIncomingAfterPublication(blockedVote, mock(Runnable.class));
handler.dispatchIncomingAfterPublication(release, mock(Runnable.class));
handler.dispatchIncomingAfterPublication(secondRelease, mock(Runnable.class));
assertEquals(2, asyncTasks.size(), "only one durable release should use the bounded release worker");
asyncTasks.removeLast().run();
@@ -425,7 +419,7 @@ class BackendProxyHandlerLifecycleTest {
doThrow(new java.util.concurrent.RejectedExecutionException("stopping"))
.when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, release, mock(Runnable.class));
handler.dispatchIncomingAfterPublication(release, mock(Runnable.class));
verify(router, never()).handleOrderedVote(eq(release), any());
AtomicBoolean active = (AtomicBoolean) getField(handler, "durableReceiptReleaseActive");
@@ -488,8 +482,8 @@ class BackendProxyHandlerLifecycleTest {
"survival", UUID.randomUUID(), VotingPluginWire.SUB_VOTE);
when(router.isValidReceiptRelease(release)).thenReturn(true);
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, olderVote, mock(Runnable.class));
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, release, mock(Runnable.class));
handler.dispatchIncomingAfterPublication(olderVote, mock(Runnable.class));
handler.dispatchIncomingAfterPublication(release, mock(Runnable.class));
@SuppressWarnings("unchecked")
ArrayDeque<JsonEnvelope> queued = (ArrayDeque<JsonEnvelope>) getField(handler, "orderedVoteDispatchQueue");
@@ -527,7 +521,7 @@ class BackendProxyHandlerLifecycleTest {
VotingPluginWire.vote("Player", UUID.randomUUID().toString(), "site", 10L,
true, true, "", voteId, false, false, 1, 1));
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, envelope, mock(Runnable.class));
handler.dispatchIncomingAfterPublication(envelope, mock(Runnable.class));
dispatch.get().run();
verifyNoInteractions(messages);
completion.get().accept(OrderedVoteOutcome.COMPLETE);
@@ -566,7 +560,7 @@ class BackendProxyHandlerLifecycleTest {
VotingPluginWire.voteDelayRejected("Player", UUID.randomUUID().toString(),
"site", true, voteId));
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, envelope, mock(Runnable.class));
handler.dispatchIncomingAfterPublication(envelope, mock(Runnable.class));
dispatch.get().run();
verifyNoInteractions(messages);
completion.get().accept(OrderedVoteOutcome.COMPLETE);
@@ -586,19 +580,19 @@ class BackendProxyHandlerLifecycleTest {
when(plugin.getLogger()).thenReturn(Logger.getLogger("ordered-overflow-test"));
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
setField(handler, "messageRouter", mock(BackendProxyMessageRouter.class));
handler.activateInboundMessages();
for (int i = 0; i < BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE + 1; i++) {
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
for (int i = 0; i < BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE + 1; i++) {
handler.dispatchIncomingAfterPublication(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(BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE, queued.size());
assertEquals(BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE, queued.size());
assertEquals(1, overflow.size());
} finally {
overflow.close();
@@ -656,14 +650,14 @@ class BackendProxyHandlerLifecycleTest {
blocker.start();
assertTrue(locked.await(3, TimeUnit.SECONDS));
try {
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
setField(handler, "messageRouter", mock(BackendProxyMessageRouter.class));
handler.activateInboundMessages();
for (int index = 0; index < BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE; index++) {
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
for (int index = 0; index < BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE; index++) {
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
.put("sequence", Integer.toString(index)).build(), mock(Runnable.class));
}
CompletableFuture<Void> admission = CompletableFuture.runAsync(() -> BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler,
CompletableFuture<Void> admission = CompletableFuture.runAsync(() -> handler.dispatchIncomingAfterPublication(
JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).put("sequence", "overflow").build(),
mock(Runnable.class)));
admission.get(500, TimeUnit.MILLISECONDS);
@@ -690,7 +684,7 @@ class BackendProxyHandlerLifecycleTest {
when(plugin.getLogger()).thenReturn(Logger.getLogger("ordered-unreadable-test"));
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
assertFalse(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
assertFalse(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
} finally {
overflow.close();
}
@@ -711,7 +705,7 @@ class BackendProxyHandlerLifecycleTest {
Files.writeString(queueFile, malformed);
BackendOrderedVoteOverflowQueue malformedQueue = new BackendOrderedVoteOverflowQueue(plugin);
try {
assertFalse(BackendOrderedVoteOverflowQueueAccess.enqueue(malformedQueue, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
assertFalse(malformedQueue.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
} finally {
malformedQueue.close();
}
@@ -725,7 +719,7 @@ class BackendProxyHandlerLifecycleTest {
Files.writeString(queueFile, unsupported);
BackendOrderedVoteOverflowQueue unsupportedQueue = new BackendOrderedVoteOverflowQueue(plugin);
try {
assertFalse(BackendOrderedVoteOverflowQueueAccess.enqueue(unsupportedQueue, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
assertFalse(unsupportedQueue.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
} finally {
unsupportedQueue.close();
}
@@ -740,7 +734,7 @@ class BackendProxyHandlerLifecycleTest {
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
Files.createDirectory(tempDir.resolve("BackendProxyVoteQueue.yml"));
assertFalse(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
assertFalse(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
assertEquals(0, overflow.size());
} finally {
overflow.close();
@@ -750,7 +744,7 @@ class BackendProxyHandlerLifecycleTest {
@Test
void oversizedFailureHistoryStopsRecoveryWithoutDiscardingEvidence(@TempDir Path tempDir) throws Exception {
StringBuilder yaml = new StringBuilder("Envelopes: []\nFailedEnvelopes:\n");
for (int index = 0; index < BackendOrderedVoteOverflowQueueAccess.MAX_FAILED_ENTRIES + 20; index++) {
for (int index = 0; index < BackendOrderedVoteOverflowQueue.MAX_FAILED_ENTRIES + 20; index++) {
yaml.append("- failed-").append(index).append('\n');
}
Files.writeString(tempDir.resolve("BackendProxyVoteQueue.yml"), yaml);
@@ -759,8 +753,8 @@ class BackendProxyHandlerLifecycleTest {
when(plugin.getLogger()).thenReturn(Logger.getLogger("ordered-failure-bound-test"));
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
assertEquals(0, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
assertFalse(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
assertEquals(0, overflow.failedSize());
assertFalse(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
assertTrue(Files.readString(tempDir.resolve("BackendProxyVoteQueue.yml")).contains("failed-0"));
} finally {
overflow.close();
@@ -770,7 +764,7 @@ class BackendProxyHandlerLifecycleTest {
@Test
void fullFailureHistoryRejectsNextQuarantineWithoutEviction(@TempDir Path tempDir) throws Exception {
StringBuilder yaml = new StringBuilder("Envelopes: []\nFailedEnvelopes:\n");
for (int index = 0; index < BackendOrderedVoteOverflowQueueAccess.MAX_FAILED_ENTRIES; index++) {
for (int index = 0; index < BackendOrderedVoteOverflowQueue.MAX_FAILED_ENTRIES; index++) {
yaml.append("- failed-").append(index).append('\n');
}
Files.writeString(tempDir.resolve("BackendProxyVoteQueue.yml"), yaml);
@@ -779,10 +773,10 @@ class BackendProxyHandlerLifecycleTest {
when(plugin.getLogger()).thenReturn(Logger.getLogger("ordered-failure-capacity-test"));
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
assertFalse(BackendOrderedVoteOverflowQueueAccess.quarantineForShutdown(overflow, null, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
assertFalse(overflow.quarantineForShutdown(null, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
.put("sequence", "rejected").build()));
assertEquals(BackendOrderedVoteOverflowQueueAccess.MAX_FAILED_ENTRIES, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
assertTrue(BackendOrderedVoteOverflowQueueAccess.isQuarantineCapacityExhausted(overflow));
assertEquals(BackendOrderedVoteOverflowQueue.MAX_FAILED_ENTRIES, overflow.failedSize());
assertTrue(overflow.isQuarantineCapacityExhausted());
@SuppressWarnings("unchecked")
ArrayDeque<String> failures = (ArrayDeque<String>) getField(overflow, "failedEntries");
assertEquals("failed-0", failures.peekFirst());
@@ -790,9 +784,9 @@ class BackendProxyHandlerLifecycleTest {
BukkitScheduler scheduler = mock(BukkitScheduler.class);
when(plugin.getBukkitScheduler()).thenReturn(scheduler);
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
handler.activateInboundMessages();
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build(),
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build(),
mock(Runnable.class));
verify(scheduler, never()).runTaskAsynchronously(eq(plugin), any(Runnable.class));
} finally {
@@ -810,19 +804,19 @@ class BackendProxyHandlerLifecycleTest {
UUID.randomUUID().toString(), "site", 10L, true, true, "", voteId, false, false, 1, 1));
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
assertTrue(BackendOrderedVoteOverflowQueueAccess.quarantineForShutdown(overflow, null, vote));
assertTrue(overflow.quarantineForShutdown(null, vote));
long firstDeadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
while (BackendOrderedVoteOverflowQueueAccess.failedSize(overflow) != 1 && System.nanoTime() < firstDeadline) Thread.sleep(10L);
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
while (overflow.failedSize() != 1 && System.nanoTime() < firstDeadline) Thread.sleep(10L);
assertEquals(1, overflow.failedSize());
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, vote));
BackendOrderedVoteOverflowQueueAccess.PendingEnvelope retry = BackendOrderedVoteOverflowQueueAccess.peekDurable(overflow);
assertTrue(overflow.enqueue(vote));
BackendOrderedVoteOverflowQueue.PendingEnvelope retry = overflow.peekDurable();
assertNotNull(retry);
CompletableFuture<Boolean> quarantined = new CompletableFuture<>();
BackendOrderedVoteOverflowQueueAccess.quarantineAsync(overflow, retry, vote, quarantined::complete);
overflow.quarantineAsync(retry, vote, quarantined::complete);
assertTrue(quarantined.get(3, TimeUnit.SECONDS));
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
assertEquals(1, overflow.failedSize());
assertEquals(0, overflow.size());
} finally {
overflow.close();
@@ -830,7 +824,7 @@ class BackendProxyHandlerLifecycleTest {
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
try {
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(recovered));
assertEquals(1, recovered.failedSize());
assertEquals(0, recovered.size());
} finally {
recovered.close();
@@ -851,7 +845,7 @@ class BackendProxyHandlerLifecycleTest {
}).when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
setField(handler, "messageRouter", router);
AtomicInteger attempts = new AtomicInteger();
@@ -862,9 +856,9 @@ class BackendProxyHandlerLifecycleTest {
return null;
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
handler.activateInboundMessages();
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
assertTrue(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
.put("sequence", "failed").build()));
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
assertTrue(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
.put("sequence", "next").build()));
Runnable first = asyncTasks.poll(3, TimeUnit.SECONDS);
assertNotNull(first);
@@ -876,7 +870,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, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
assertEquals(1, overflow.failedSize());
assertTrue(Files.exists(tempDir.resolve("BackendProxyVoteQueue.yml")));
} finally {
overflow.close();
@@ -884,7 +878,7 @@ class BackendProxyHandlerLifecycleTest {
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
try {
assertEquals(0, recovered.size());
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(recovered));
assertEquals(1, recovered.failedSize());
} finally {
recovered.close();
}
@@ -904,22 +898,22 @@ class BackendProxyHandlerLifecycleTest {
}).when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
setField(handler, "messageRouter", router);
doThrow(new IllegalStateException("partial reward")).when(router)
.handleOrderedVote(any(JsonEnvelope.class), any());
handler.activateInboundMessages();
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
handler.dispatchIncomingAfterPublication(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 (BackendOrderedVoteOverflowQueueAccess.failedSize(overflow) == 0 && System.nanoTime() < deadline) {
while (overflow.failedSize() == 0 && System.nanoTime() < deadline) {
Thread.sleep(10);
}
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
assertEquals(1, overflow.failedSize());
assertEquals(0, overflow.size());
assertTrue(asyncTasks.isEmpty(), "failed reward must not be retried");
} finally {
@@ -953,13 +947,13 @@ class BackendProxyHandlerLifecycleTest {
});
assertTrue(occupied.await(3, TimeUnit.SECONDS));
try {
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
setField(handler, "messageRouter", router);
doThrow(new IllegalStateException("partial reward")).when(router)
.handleOrderedVote(any(JsonEnvelope.class), any());
handler.activateInboundMessages();
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
.put("sequence", "failed").build(), mock(Runnable.class));
Runnable first = asyncTasks.poll(3, TimeUnit.SECONDS);
assertNotNull(first);
@@ -972,7 +966,7 @@ class BackendProxyHandlerLifecycleTest {
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
try {
assertEquals(0, recovered.size());
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(recovered));
assertEquals(1, recovered.failedSize());
} finally {
recovered.close();
}
@@ -986,29 +980,29 @@ class BackendProxyHandlerLifecycleTest {
when(plugin.getLogger()).thenReturn(Logger.getLogger("ordered-capacity-test"));
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
handler.activateInboundMessages();
for (int index = 0; index < BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE; index++) {
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
for (int index = 0; index < BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE; index++) {
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
.put("sequence", Integer.toString(index)).build(), mock(Runnable.class));
}
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));
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));
}
assertFalse(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build(),
BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE));
assertFalse(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build(),
BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE));
handler.close();
assertEquals(BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE
+ BackendOrderedVoteOverflowQueueAccess.MAX_NORMAL_ENTRIES, overflow.size());
assertEquals(BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE
+ BackendOrderedVoteOverflowQueue.MAX_NORMAL_ENTRIES, overflow.size());
} finally {
overflow.close();
}
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
try {
assertEquals(BackendProxyHandlerAccess.MAX_ORDERED_VOTE_QUEUE
+ BackendOrderedVoteOverflowQueueAccess.MAX_NORMAL_ENTRIES, recovered.size());
assertEquals(BackendProxyHandler.MAX_ORDERED_VOTE_QUEUE
+ BackendOrderedVoteOverflowQueue.MAX_NORMAL_ENTRIES, recovered.size());
} finally {
recovered.close();
}
@@ -1028,7 +1022,7 @@ class BackendProxyHandlerLifecycleTest {
}).when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
setField(handler, "messageRouter", router);
AtomicInteger attempts = new AtomicInteger();
@@ -1039,7 +1033,7 @@ class BackendProxyHandlerLifecycleTest {
return null;
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
handler.activateInboundMessages();
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build()));
assertTrue(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build()));
Runnable first = asyncTasks.poll(3, TimeUnit.SECONDS);
assertNotNull(first, "durable head must wake the ordered lane");
@@ -1073,7 +1067,7 @@ class BackendProxyHandlerLifecycleTest {
}).when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
setField(handler, "messageRouter", router);
doAnswer(invocation -> {
@@ -1082,7 +1076,7 @@ class BackendProxyHandlerLifecycleTest {
return null;
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
handler.activateInboundMessages();
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build()));
assertTrue(overflow.enqueue(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);
@@ -1124,11 +1118,11 @@ class BackendProxyHandlerLifecycleTest {
try {
overflow.close();
assertTrue(System.nanoTime() - started < TimeUnit.SECONDS.toNanos(4));
assertEquals(BackendOrderedVoteOverflowQueueAccess.CloseState.WAITING_TO_PERSIST, BackendOrderedVoteOverflowQueueAccess.closeState(overflow));
assertEquals(BackendOrderedVoteOverflowQueue.CloseState.WAITING_TO_PERSIST, overflow.closeState());
} finally {
release.countDown();
blocker.join(3_000L);
assertTrue(BackendOrderedVoteOverflowQueueAccess.awaitClose(overflow, 3_000L));
assertTrue(overflow.awaitClose(3_000L));
}
}
@@ -1143,7 +1137,7 @@ class BackendProxyHandlerLifecycleTest {
when(plugin.getDataFolder()).thenReturn(tempDir.toFile());
when(plugin.getLogger()).thenReturn(logger);
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
assertTrue(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).put("sequence", "slow").build()));
assertTrue(overflow.enqueue(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).put("sequence", "slow").build()));
Object persistenceLock = getField(overflow, "persistenceWriteLock");
CountDownLatch locked = new CountDownLatch(1);
Thread blocker = new Thread(() -> {
@@ -1162,7 +1156,7 @@ class BackendProxyHandlerLifecycleTest {
overflow.close();
blocker.join(3_000L);
assertTrue(System.nanoTime() - started >= TimeUnit.SECONDS.toNanos(1));
assertEquals(BackendOrderedVoteOverflowQueueAccess.CloseState.COMPLETE, BackendOrderedVoteOverflowQueueAccess.closeState(overflow));
assertEquals(BackendOrderedVoteOverflowQueue.CloseState.COMPLETE, overflow.closeState());
assertFalse(logs.hasLevel(Level.SEVERE));
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
@@ -1189,7 +1183,7 @@ class BackendProxyHandlerLifecycleTest {
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
overflow.close();
assertEquals(BackendOrderedVoteOverflowQueueAccess.CloseState.FAILED, BackendOrderedVoteOverflowQueueAccess.closeState(overflow));
assertEquals(BackendOrderedVoteOverflowQueue.CloseState.FAILED, overflow.closeState());
assertTrue(logs.hasLevel(Level.SEVERE));
LogRecord failure = logs.records.stream()
.filter(record -> record.getLevel() == Level.SEVERE)
@@ -1229,9 +1223,9 @@ class BackendProxyHandlerLifecycleTest {
return null;
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
handler.activateInboundMessages();
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE)
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE)
.put("sequence", "first").build(), mock(Runnable.class));
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
.put("sequence", "second").build(), mock(Runnable.class));
asyncTasks.removeFirst().run();
@@ -1252,12 +1246,12 @@ class BackendProxyHandlerLifecycleTest {
when(plugin.getLogger()).thenReturn(Logger.getLogger("ordered-shutdown-test"));
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
BackendProxyTransportManager transport = mock(BackendProxyTransportManager.class);
setField(handler, "transportManager", transport);
handler.activateInboundMessages();
doAnswer(invocation -> {
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler,
handler.dispatchIncomingAfterPublication(
JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).build(), mock(Runnable.class));
return null;
}).when(transport).close();
@@ -1289,7 +1283,7 @@ class BackendProxyHandlerLifecycleTest {
}).when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
try {
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
setField(handler, "messageRouter", router);
AtomicReference<java.util.function.Consumer<OrderedVoteOutcome>> completion = new AtomicReference<>();
@@ -1298,9 +1292,9 @@ class BackendProxyHandlerLifecycleTest {
return null;
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
handler.activateInboundMessages();
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
.put("sequence", "active").build(), mock(Runnable.class));
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
.put("sequence", "later").build(), mock(Runnable.class));
asyncTasks.removeFirst().run();
CompletableFuture<Void> closing = CompletableFuture.runAsync(handler::close);
@@ -1329,7 +1323,7 @@ class BackendProxyHandlerLifecycleTest {
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
AtomicReference<java.util.function.Consumer<OrderedVoteOutcome>> completion = new AtomicReference<>();
try {
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), overflow);
BackendProxyMessageRouter router = mock(BackendProxyMessageRouter.class);
setField(handler, "messageRouter", router);
doAnswer(invocation -> {
@@ -1337,26 +1331,26 @@ class BackendProxyHandlerLifecycleTest {
return null;
}).when(router).handleOrderedVote(any(JsonEnvelope.class), any());
handler.activateInboundMessages();
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
handler.dispatchIncomingAfterPublication(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
.put("sequence", "active").build(), mock(Runnable.class));
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
handler.dispatchIncomingAfterPublication(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 (BackendOrderedVoteOverflowQueueAccess.failedSize(overflow) != 1 && System.nanoTime() < quarantineDeadline) Thread.sleep(10L);
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
while (overflow.failedSize() != 1 && System.nanoTime() < quarantineDeadline) Thread.sleep(10L);
assertEquals(1, overflow.failedSize());
assertEquals(1, overflow.size());
completion.get().accept(OrderedVoteOutcome.COMPLETE);
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
assertEquals(1, overflow.failedSize());
assertEquals(1, overflow.size());
} finally {
overflow.close();
}
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
try {
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(recovered));
assertEquals(1, recovered.failedSize());
assertEquals(1, recovered.size());
} finally {
recovered.close();
@@ -1378,32 +1372,32 @@ class BackendProxyHandlerLifecycleTest {
BackendOrderedVoteOverflowQueue overflow = new BackendOrderedVoteOverflowQueue(plugin);
AtomicReference<java.util.function.Consumer<OrderedVoteOutcome>> completion = new AtomicReference<>();
try {
BackendProxyHandler handler = new BackendProxyHandler(plugin, ProcessedVoteCacheAccess.create(), overflow);
BackendProxyHandler handler = new BackendProxyHandler(plugin, new ProcessedVoteCache(), 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(BackendOrderedVoteOverflowQueueAccess.enqueue(overflow, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
assertTrue(overflow.enqueue(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 (BackendOrderedVoteOverflowQueueAccess.failedSize(overflow) != 1 && System.nanoTime() < quarantineDeadline) Thread.sleep(10L);
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
while (overflow.failedSize() != 1 && System.nanoTime() < quarantineDeadline) Thread.sleep(10L);
assertEquals(1, overflow.failedSize());
assertEquals(0, overflow.size());
completion.get().accept(OrderedVoteOutcome.COMPLETE);
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(overflow));
assertEquals(1, overflow.failedSize());
assertEquals(0, overflow.size());
} finally {
overflow.close();
}
BackendOrderedVoteOverflowQueue recovered = new BackendOrderedVoteOverflowQueue(plugin);
try {
assertEquals(1, BackendOrderedVoteOverflowQueueAccess.failedSize(recovered));
assertEquals(1, recovered.failedSize());
assertEquals(0, recovered.size());
} finally {
recovered.close();
@@ -1425,12 +1419,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();
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(previous, first, mock(Runnable.class));
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(previous, second, mock(Runnable.class));
previous.dispatchIncomingAfterPublication(first, mock(Runnable.class));
previous.dispatchIncomingAfterPublication(second, mock(Runnable.class));
previous.completeOrderedVoteHandoff(replacement);
JsonEnvelope late = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_ONLINE).put("sequence", "3").build();
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(previous, late, mock(Runnable.class));
previous.dispatchIncomingAfterPublication(late, mock(Runnable.class));
ArrayDeque<Runnable> asyncTasks = new ArrayDeque<>();
doAnswer(invocation -> {
@@ -1504,7 +1498,7 @@ class BackendProxyHandlerLifecycleTest {
CompletableFuture<Void> callback = CompletableFuture.runAsync(() -> {
started.countDown();
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(handler, envelope, localDispatch);
handler.dispatchIncomingAfterPublication(envelope, localDispatch);
});
assertTrue(started.await(1, TimeUnit.SECONDS));
assertFalse(callback.isDone());
@@ -1530,7 +1524,7 @@ class BackendProxyHandlerLifecycleTest {
CompletableFuture<Void> callback = CompletableFuture.runAsync(() -> {
started.countDown();
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(replacement, envelope, replacementDispatch);
replacement.dispatchIncomingAfterPublication(envelope, replacementDispatch);
});
assertTrue(started.await(1, TimeUnit.SECONDS));
assertFalse(callback.isDone());
@@ -1562,7 +1556,7 @@ class BackendProxyHandlerLifecycleTest {
CompletableFuture<Void> callback = CompletableFuture.runAsync(() -> {
started.countDown();
BackendProxyHandlerAccess.dispatchIncomingAfterPublication(replacement, envelope, replacementDispatch);
replacement.dispatchIncomingAfterPublication(envelope, replacementDispatch);
});
assertTrue(started.await(1, TimeUnit.SECONDS));
assertFalse(callback.isDone());
@@ -1614,7 +1608,7 @@ class BackendProxyHandlerLifecycleTest {
try {
Field transportField = BackendProxyTransportManager.class.getDeclaredField("transport");
transportField.setAccessible(true);
ReflectiveAccess.set(transportField, manager, previous);
transportField.set(manager, previous);
manager.restoreAfterFailedReplacement();
} catch (ReflectiveOperationException failure) {
throw new AssertionError(failure);
@@ -1683,7 +1677,7 @@ class BackendProxyHandlerLifecycleTest {
Field targetField = com.bencodez.votingplugin.VotingPluginMain.class
.getDeclaredField("backendPluginMessageTarget");
targetField.setAccessible(true);
ReflectiveAccess.set(targetField, plugin, new AtomicReference<GlobalMessageHandler>());
targetField.set(plugin, new AtomicReference<GlobalMessageHandler>());
plugin.activateBackendPluginMessageHandler(firstTarget);
current.set(second);
@@ -1870,7 +1864,7 @@ class BackendProxyHandlerLifecycleTest {
BackendPresenceManager presence = mock(BackendPresenceManager.class);
Field field = BackendProxyHandler.class.getDeclaredField("presenceManager");
field.setAccessible(true);
ReflectiveAccess.set(field, handler, presence);
field.set(handler, presence);
verifyNoInteractions(presence);
handler.activatePresenceReporting();
@@ -1886,7 +1880,7 @@ class BackendProxyHandlerLifecycleTest {
org.mockito.Mockito.doThrow(new java.util.concurrent.RejectedExecutionException()).when(presence).start();
Field presenceField = BackendProxyHandler.class.getDeclaredField("presenceManager");
presenceField.setAccessible(true);
ReflectiveAccess.set(presenceField, handler, presence);
presenceField.set(handler, presence);
Field managerField = BackendProxyHandler.class.getDeclaredField("transportManager");
managerField.setAccessible(true);
@@ -1894,7 +1888,7 @@ class BackendProxyHandlerLifecycleTest {
BackendProxyTransport transport = mock(BackendProxyTransport.class);
Field transportField = BackendProxyTransportManager.class.getDeclaredField("transport");
transportField.setAccessible(true);
ReflectiveAccess.set(transportField, manager, transport);
transportField.set(manager, transport);
assertThrows(java.util.concurrent.RejectedExecutionException.class, handler::activatePresenceReporting);
@@ -1904,7 +1898,7 @@ class BackendProxyHandlerLifecycleTest {
@Test
void sharesVoteDeduplicationAcrossHandlerReplacement() {
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
ProcessedVoteCache cache = new ProcessedVoteCache();
BackendProxyHandler previous = new BackendProxyHandler(null, cache);
BackendProxyHandler replacement = new BackendProxyHandler(null, cache);
@@ -1923,7 +1917,7 @@ class BackendProxyHandlerLifecycleTest {
BackendProxyTransport transport = mock(BackendProxyTransport.class);
Field transportField = BackendProxyTransportManager.class.getDeclaredField("transport");
transportField.setAccessible(true);
ReflectiveAccess.set(transportField, manager, transport);
transportField.set(manager, transport);
handler.prepareForReplacement(BungeeMethod.PLUGINMESSAGING);
@@ -1936,7 +1930,7 @@ class BackendProxyHandlerLifecycleTest {
SocketHandler socket = mock(SocketHandler.class);
Field field = SocketBackendProxyTransport.class.getDeclaredField("socketHandler");
field.setAccessible(true);
ReflectiveAccess.set(field, handler, socket);
field.set(handler, socket);
handler.prepareForReplacement();
@@ -2023,10 +2017,10 @@ class BackendProxyHandlerLifecycleTest {
RedisHandler redis = mock(RedisHandler.class);
Field field = RedisBackendProxyTransport.class.getDeclaredField("redisHandler");
field.setAccessible(true);
ReflectiveAccess.set(field, transport, redis);
field.set(transport, redis);
Field transportField = BackendProxyTransportManager.class.getDeclaredField("transport");
transportField.setAccessible(true);
ReflectiveAccess.set(transportField, manager, transport);
transportField.set(manager, transport);
handler.prepareForReplacement(BungeeMethod.REDIS);
@@ -2040,7 +2034,7 @@ class BackendProxyHandlerLifecycleTest {
MqttHandler mqtt = mock(MqttHandler.class);
Field field = MqttBackendProxyTransport.class.getDeclaredField("mqttHandler");
field.setAccessible(true);
ReflectiveAccess.set(field, handler, mqtt);
field.set(handler, mqtt);
handler.prepareForReplacement();
@@ -2772,7 +2766,7 @@ class BackendProxyHandlerLifecycleTest {
MySqlMessenger messenger = mock(MySqlMessenger.class);
Field field = MysqlBackendProxyTransport.class.getDeclaredField("messenger");
field.setAccessible(true);
ReflectiveAccess.set(field, handler, messenger);
field.set(handler, messenger);
handler.prepareForReplacement();
@@ -2917,7 +2911,7 @@ class BackendProxyHandlerLifecycleTest {
ScheduledExecutorService timer = mock(ScheduledExecutorService.class);
Field field = BackendGlobalDataSync.class.getDeclaredField("timer");
field.setAccessible(true);
ReflectiveAccess.set(field, handler, timer);
field.set(handler, timer);
handler.close();
@@ -2950,7 +2944,7 @@ class BackendProxyHandlerLifecycleTest {
try {
Field field = type.getDeclaredField(name);
field.setAccessible(true);
ReflectiveAccess.set(field, target, value);
field.set(target, value);
return;
} catch (NoSuchFieldException ignored) {
type = type.getSuperclass();
@@ -1,7 +1,4 @@
package com.bencodez.votingplugin.tests.backendproxy.cache;
import com.bencodez.votingplugin.tests.backendproxy.cache.DurableVoteReceiptStoreAccess;
import com.bencodez.votingplugin.backendproxy.cache.ProcessedVoteCache;
package com.bencodez.votingplugin.backendproxy.cache;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -19,7 +16,7 @@ class ProcessedVoteCacheDurabilityTest {
@Test
void duplicateRetryDoesNotScanAndPruneUnrelatedEntries() {
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(TimeUnit.MINUTES.toMillis(30), 2);
ProcessedVoteCache cache = new ProcessedVoteCache(TimeUnit.MINUTES.toMillis(30), 2);
UUID expired = UUID.randomUUID();
UUID live = UUID.randomUUID();
cache.getProcessedVotes().put(expired, 0L);
@@ -32,7 +29,7 @@ class ProcessedVoteCacheDurabilityTest {
@Test
void expiredHeadRetryReclaimsItsSlotBeforeSaturationCheck() {
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(TimeUnit.MINUTES.toMillis(30), 1);
ProcessedVoteCache cache = new ProcessedVoteCache(TimeUnit.MINUTES.toMillis(30), 1);
UUID expired = UUID.randomUUID();
cache.getProcessedVotes().put(expired, 0L);
@@ -47,44 +44,44 @@ class ProcessedVoteCacheDurabilityTest {
void releaseTombstoneCapacityCoversSustainedVoteThroughputForFullTtl() {
long threeVotesPerSecondForOneDay = TimeUnit.DAYS.toSeconds(1) * 3;
assertTrue(DurableVoteReceiptStoreAccess.MAX_RELEASE_TOMBSTONES >= threeVotesPerSecondForOneDay);
assertTrue(DurableVoteReceiptStore.MAX_RELEASE_TOMBSTONES >= threeVotesPerSecondForOneDay);
}
@Test
void completedVoteRemainsDeduplicatedAfterBackendRestart() {
Path receipts = directory.resolve("receipts.dat");
UUID voteId = UUID.randomUUID();
ProcessedVoteCache first = ProcessedVoteCacheAccess.create(receipts);
ProcessedVoteCache first = new ProcessedVoteCache(receipts);
assertTrue(first.reserve(voteId));
assertTrue(first.complete(voteId));
assertTrue(first.hasDurableReceipt(voteId));
assertFalse(ProcessedVoteCacheAccess.create(receipts).reserve(voteId));
assertFalse(new ProcessedVoteCache(receipts).reserve(voteId));
}
@Test
void reservationWithoutCompletionDoesNotSuppressRestartRetry() {
Path receipts = directory.resolve("receipts.dat");
UUID voteId = UUID.randomUUID();
assertTrue(ProcessedVoteCacheAccess.create(receipts).reserve(voteId));
assertTrue(new ProcessedVoteCache(receipts).reserve(voteId));
assertTrue(ProcessedVoteCacheAccess.create(receipts).reserve(voteId));
assertTrue(new ProcessedVoteCache(receipts).reserve(voteId));
}
@Test
void repairsTruncatedTailBeforeAppendingAnotherReceipt() throws Exception {
Path receipts = directory.resolve("receipts.dat");
UUID firstId = UUID.randomUUID();
ProcessedVoteCache first = ProcessedVoteCacheAccess.create(receipts);
ProcessedVoteCache first = new ProcessedVoteCache(receipts);
assertTrue(first.reserve(firstId));
assertTrue(first.complete(firstId));
Files.writeString(receipts, "truncated", StandardOpenOption.APPEND);
ProcessedVoteCache repaired = ProcessedVoteCacheAccess.create(receipts);
ProcessedVoteCache repaired = new ProcessedVoteCache(receipts);
UUID secondId = UUID.randomUUID();
assertTrue(repaired.reserve(secondId));
assertTrue(repaired.complete(secondId));
ProcessedVoteCache restarted = ProcessedVoteCacheAccess.create(receipts);
ProcessedVoteCache restarted = new ProcessedVoteCache(receipts);
assertFalse(restarted.reserve(firstId));
assertFalse(restarted.reserve(secondId));
}
@@ -94,14 +91,14 @@ class ProcessedVoteCacheDurabilityTest {
Path receipts = directory.resolve("receipts.dat");
UUID firstId = UUID.randomUUID();
UUID secondId = UUID.randomUUID();
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(receipts);
ProcessedVoteCache cache = new ProcessedVoteCache(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 = ProcessedVoteCacheAccess.create(receipts);
ProcessedVoteCache restarted = new ProcessedVoteCache(receipts);
assertFalse(restarted.reserve(firstId));
assertFalse(restarted.reserve(secondId));
}
@@ -112,7 +109,7 @@ class ProcessedVoteCacheDurabilityTest {
UUID incompleteId = UUID.randomUUID();
Files.writeString(receipts, "VP-VOTE-RECEIPTS-1\n" + incompleteId + "\t9");
ProcessedVoteCache repaired = ProcessedVoteCacheAccess.create(receipts);
ProcessedVoteCache repaired = new ProcessedVoteCache(receipts);
assertTrue(repaired.reserve(incompleteId));
assertTrue(Files.readString(receipts).endsWith("\n"));
@@ -123,7 +120,7 @@ class ProcessedVoteCacheDurabilityTest {
Path unusableParent = directory.resolve("not-a-directory");
Files.writeString(unusableParent, "file");
UUID voteId = UUID.randomUUID();
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(1L, unusableParent.resolve("receipts.dat"));
ProcessedVoteCache cache = new ProcessedVoteCache(1L, unusableParent.resolve("receipts.dat"));
assertTrue(cache.reserve(voteId));
assertFalse(cache.complete(voteId));
@@ -135,31 +132,31 @@ class ProcessedVoteCacheDurabilityTest {
void proxyConfirmedReleaseLeavesRestartSafeTombstone() {
Path receipts = directory.resolve("receipts.dat");
UUID voteId = UUID.randomUUID();
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(receipts);
ProcessedVoteCache cache = new ProcessedVoteCache(receipts);
assertTrue(cache.reserve(voteId));
assertTrue(cache.complete(voteId));
assertTrue(cache.releaseCompletedReceipt(voteId));
assertFalse(ProcessedVoteCacheAccess.create(receipts).reserve(voteId));
assertFalse(new ProcessedVoteCache(receipts).reserve(voteId));
}
@Test
void unknownReceiptReleaseLeavesRestartSafeTombstone() {
Path receipts = directory.resolve("receipts.dat");
UUID voteId = UUID.randomUUID();
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(receipts);
ProcessedVoteCache cache = new ProcessedVoteCache(receipts);
assertFalse(cache.hasDurableReceipt(voteId));
assertTrue(cache.releaseCompletedReceipt(voteId));
assertTrue(cache.hasDurableReceipt(voteId));
assertFalse(ProcessedVoteCacheAccess.create(receipts).reserve(voteId));
assertFalse(new ProcessedVoteCache(receipts).reserve(voteId));
}
@Test
void completionHeadroomAllowsPrecedingVotesBeforeRelease() throws Exception {
DurableVoteReceiptStoreAccess store = new DurableVoteReceiptStoreAccess(directory.resolve("receipts.dat"), 1, 2, 1);
DurableVoteReceiptStore store = new DurableVoteReceiptStore(directory.resolve("receipts.dat"), 1, 2, 1);
UUID first = UUID.randomUUID();
UUID second = UUID.randomUUID();
UUID third = UUID.randomUUID();
@@ -176,14 +173,14 @@ class ProcessedVoteCacheDurabilityTest {
@Test
void activeReceiptWaitsWhenTombstoneCapacityIsFull() throws Exception {
Path receipts = directory.resolve("receipts.dat");
DurableVoteReceiptStoreAccess store = new DurableVoteReceiptStoreAccess(receipts, 1, 0, 1);
DurableVoteReceiptStore store = new DurableVoteReceiptStore(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 DurableVoteReceiptStoreAccess(receipts, 1, 0, 1);
new DurableVoteReceiptStore(receipts, 1, 0, 1);
}
@Test
@@ -192,12 +189,12 @@ class ProcessedVoteCacheDurabilityTest {
UUID voteId = UUID.randomUUID();
Files.writeString(receipts, "VP-VOTE-RECEIPTS-1\nR\t" + voteId + "\t1\n");
assertTrue(ProcessedVoteCacheAccess.create(receipts).reserve(voteId));
assertTrue(new ProcessedVoteCache(receipts).reserve(voteId));
}
@Test
void saturationDoesNotEvictLiveReplayFences() {
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(TimeUnit.MINUTES.toMillis(30), 2);
ProcessedVoteCache cache = new ProcessedVoteCache(TimeUnit.MINUTES.toMillis(30), 2);
UUID first = UUID.randomUUID();
UUID second = UUID.randomUUID();
@@ -211,19 +208,19 @@ class ProcessedVoteCacheDurabilityTest {
@Test
void defaultCapacityIncludesDurableCompletionHeadroom() {
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);
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);
}
@Test
void mixedActiveAndReleaseReceiptsRespectTheJournalByteAlignedLimit() throws Exception {
DurableVoteReceiptStoreAccess store = new DurableVoteReceiptStoreAccess(directory.resolve("mixed-receipts.dat"), 2, 0, 2, 2);
DurableVoteReceiptStore store = new DurableVoteReceiptStore(directory.resolve("mixed-receipts.dat"), 2, 0, 2, 2);
UUID released = UUID.randomUUID();
UUID active = UUID.randomUUID();
@@ -247,7 +244,7 @@ class ProcessedVoteCacheDurabilityTest {
+ "R\t" + firstReleased + "\t" + expiresAt + "\n"
+ "R\t" + secondReleased + "\t" + expiresAt + "\n");
DurableVoteReceiptStoreAccess store = new DurableVoteReceiptStoreAccess(receipts, 2, 0, 2, 2);
DurableVoteReceiptStore store = new DurableVoteReceiptStore(receipts, 2, 0, 2, 2);
assertTrue(store.contains(firstActive));
assertTrue(store.contains(secondActive));
@@ -258,11 +255,11 @@ class ProcessedVoteCacheDurabilityTest {
@Test
void mixedJournalUsesDurablePerKindCapacityForNewAdmission() throws Exception {
DurableVoteReceiptStoreAccess store = new DurableVoteReceiptStoreAccess(directory.resolve("mixed-capacity.dat"), 2, 1, 2, 4);
DurableVoteReceiptStore store = new DurableVoteReceiptStore(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 = ProcessedVoteCacheAccess.create(TimeUnit.MINUTES.toMillis(30), 2, store);
ProcessedVoteCache cache = new ProcessedVoteCache(TimeUnit.MINUTES.toMillis(30), 2, store);
UUID admitted = UUID.randomUUID();
UUID stale = UUID.randomUUID();
cache.getProcessedVotes().put(stale, 0L);
@@ -276,7 +273,7 @@ class ProcessedVoteCacheDurabilityTest {
@Test
void cancelledValidationReservationFreesCapacity() {
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create(TimeUnit.MINUTES.toMillis(30), 1);
ProcessedVoteCache cache = new ProcessedVoteCache(TimeUnit.MINUTES.toMillis(30), 1);
UUID invalid = UUID.randomUUID();
assertTrue(cache.reserve(invalid));
@@ -1,8 +1,4 @@
package com.bencodez.votingplugin.tests.backendproxy.global;
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
import com.bencodez.votingplugin.backendproxy.global.BackendGlobalDataSync;
package com.bencodez.votingplugin.backendproxy.global;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -291,7 +287,7 @@ class BackendGlobalDataSyncTest {
oldSync.checkGlobalData();
org.junit.jupiter.api.Assertions.assertEquals(1, schedulerRuns.get());
oldSync.handoffCompletionSender(replacement);
BackendGlobalDataSyncAccess.close(oldSync, 1, TimeUnit.MILLISECONDS);
oldSync.close(1, TimeUnit.MILLISECONDS);
replacement.checkGlobalData();
org.junit.jupiter.api.Assertions.assertEquals(1, schedulerRuns.get());
@@ -404,7 +400,7 @@ class BackendGlobalDataSyncTest {
CompletableFuture<Void> execution = CompletableFuture.runAsync(asyncWrite.get());
assertTrue(writeStarted.await(1, TimeUnit.SECONDS));
oldSync.handoffCompletionSender(replacement);
BackendGlobalDataSyncAccess.close(oldSync, 1, TimeUnit.MILLISECONDS);
oldSync.close(1, TimeUnit.MILLISECONDS);
replacement.checkGlobalData();
verify(replacementHandler).setBoolean("lobby", "ForceUpdate", false);
@@ -555,7 +551,7 @@ class BackendGlobalDataSyncTest {
setField(sync, "globalDataHandler", handler);
setField(sync, "ownsGlobalMysql", false);
CompletableFuture<Void> heartbeat = CompletableFuture.runAsync(() -> BackendGlobalDataSyncAccess.updateLastOnline(sync));
CompletableFuture<Void> heartbeat = CompletableFuture.runAsync(sync::updateLastOnline);
assertTrue(heartbeatStarted.await(1, TimeUnit.SECONDS));
CompletableFuture<Void> close = CompletableFuture.runAsync(sync::close);
Thread.sleep(50L);
@@ -965,7 +961,7 @@ class BackendGlobalDataSyncTest {
assertTrue(oldSync.checkGlobalDataTime(TimeType.DAY, data));
scheduled.get().run();
oldSync.handoffCompletionSender(replacement);
BackendGlobalDataSyncAccess.close(oldSync, 1, TimeUnit.MILLISECONDS);
oldSync.close(1, TimeUnit.MILLISECONDS);
replacement.checkGlobalData();
verify(timeChecker).forceChanged(TimeType.DAY, false, true, true);
@@ -1087,7 +1083,7 @@ class BackendGlobalDataSyncTest {
week.put(TimeType.WEEK.toString(), new DataValueBoolean(true));
assertTrue(oldSync.checkGlobalDataTime(TimeType.DAY, day));
BackendGlobalDataSyncAccess.close(oldSync, 1, TimeUnit.MILLISECONDS);
oldSync.close(1, TimeUnit.MILLISECONDS);
assertFalse(replacement.checkGlobalDataTime(TimeType.DAY, day));
assertTrue(replacement.checkGlobalDataTime(TimeType.WEEK, week));
org.junit.jupiter.api.Assertions.assertEquals(2, scheduled.size());
@@ -1128,7 +1124,7 @@ class BackendGlobalDataSyncTest {
week.put(TimeType.WEEK.toString(), new DataValueBoolean(true));
assertTrue(oldSync.checkGlobalDataTime(TimeType.DAY, day));
BackendGlobalDataSyncAccess.close(oldSync, 1, TimeUnit.MILLISECONDS);
oldSync.close(1, TimeUnit.MILLISECONDS);
BackendGlobalDataSync replacement = new BackendGlobalDataSync(plugin, ignored -> { });
setField(replacement, "globalDataHandler", replacementHandler);
@@ -1223,7 +1219,7 @@ class BackendGlobalDataSyncTest {
() -> sync.checkGlobalDataTime(TimeType.DAY, data));
assertTrue(writeStarted.await(1, TimeUnit.SECONDS));
BackendGlobalDataSyncAccess.close(sync, 50L, TimeUnit.MILLISECONDS);
sync.close(50L, TimeUnit.MILLISECONDS);
verify(mysql).close();
releaseWrite.countDown();
@@ -1243,7 +1239,7 @@ class BackendGlobalDataSyncTest {
setField(sync, "globalDataHandler", handler);
setField(sync, "ownsGlobalMysql", false);
BackendGlobalDataSyncAccess.close(sync);
sync.close();
verify(mysql, never()).close();
}
@@ -1258,7 +1254,7 @@ class BackendGlobalDataSyncTest {
setField(sync, "globalDataHandler", handler);
setField(sync, "ownsGlobalMysql", true);
BackendGlobalDataSyncAccess.close(sync);
sync.close();
verify(mysql).close();
}
@@ -1267,7 +1263,7 @@ class BackendGlobalDataSyncTest {
try {
Field field = BackendGlobalDataSync.class.getDeclaredField(fieldName);
field.setAccessible(true);
ReflectiveAccess.set(field, target, value);
field.set(target, value);
} catch (ReflectiveOperationException failure) {
throw new AssertionError(failure);
}
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.backendproxy.messaging;
import com.bencodez.votingplugin.backendproxy.messaging.BackendProxyMessageRouter;
package com.bencodez.votingplugin.backendproxy.messaging;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -116,7 +114,7 @@ class BackendProxyMessageRouterTest {
@Test
void ignoresLastVoteTimeForUnknownServiceSite() {
BackendProxyMessageRouterAccess.handleVoteUpdate(router, VotingPluginWire.voteUpdate(PLAYER_UUID.toString(), 1, 10,
router.handleVoteUpdate(VotingPluginWire.voteUpdate(PLAYER_UUID.toString(), 1, 10,
"unknown.example\nforged-entry", LAST_VOTE_TIME, ""));
verify(user, times(2)).cache();
@@ -132,7 +130,7 @@ class BackendProxyMessageRouterTest {
VoteSite voteSite = mock(VoteSite.class);
when(voteSiteManager.getVoteSite("known.example", true)).thenReturn(voteSite);
BackendProxyMessageRouterAccess.handleVoteUpdate(router, VotingPluginWire.voteUpdate(PLAYER_UUID.toString(), 1, 10,
router.handleVoteUpdate(VotingPluginWire.voteUpdate(PLAYER_UUID.toString(), 1, 10,
"known.example", LAST_VOTE_TIME, ""));
verify(user, times(2)).cache();
@@ -789,7 +787,7 @@ class BackendProxyMessageRouterTest {
}).when(coreUserManager).getUserAsync(eq(PLAYER_UUID), any(), any());
AtomicInteger completions = new AtomicInteger();
BackendProxyMessageRouterAccess.handleVoteUpdate(router, VotingPluginWire.voteUpdate(PLAYER_UUID.toString(), 1, 10,
router.handleVoteUpdate(VotingPluginWire.voteUpdate(PLAYER_UUID.toString(), 1, 10,
"known.example", LAST_VOTE_TIME, ""), completions::incrementAndGet);
assertEquals(1, completions.get());
@@ -1,8 +1,4 @@
package com.bencodez.votingplugin.tests.backendproxy.messaging;
import com.bencodez.votingplugin.tests.backendproxy.cache.ProcessedVoteCacheAccess;
import com.bencodez.votingplugin.backendproxy.messaging.BackendProxyMessageRouter;
package com.bencodez.votingplugin.backendproxy.messaging;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -96,7 +92,7 @@ class BackendProxyVoteDelayReplayTest {
fixture.assertEffects(1);
fixture.assertOccurrence(0, voteId, RECORDED_TIME - 60_000L);
verify(fixture.site, never()).giveWaitUntilVoteDelayRewards(any(), anyBoolean(), anyBoolean());
assertTrue(ProcessedVoteCacheAccess.create(fixture.receipts).hasDurableReceipt(voteId));
assertTrue(new ProcessedVoteCache(fixture.receipts).hasDurableReceipt(voteId));
});
}
@@ -281,7 +277,7 @@ class BackendProxyVoteDelayReplayTest {
private void restartReceiver() {
router = new BackendProxyMessageRouter(plugin, mock(BackendPresenceManager.class),
mock(BackendGlobalDataSync.class), mock(BackendVotePartySync.class),
ProcessedVoteCacheAccess.create(receipts));
new ProcessedVoteCache(receipts));
}
private void deliver(JsonEnvelope envelope) {
@@ -1,10 +1,4 @@
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;
package com.bencodez.votingplugin.backendproxy.transport;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -70,7 +64,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();
HttpBackendProxyTransportAccess.dispatchIncoming(new HttpBackendProxyTransport(plugin), handler, envelope,
new HttpBackendProxyTransport(plugin).dispatchIncoming(handler, envelope,
System.nanoTime() + TimeUnit.SECONDS.toNanos(1));
assertTrue(callbackFinished.await(1, TimeUnit.SECONDS));
@@ -79,7 +73,7 @@ class HttpBackendProxyTransportTest {
@Test
void publicationActivatesTheSimpleApiInboundBarrier() throws Exception {
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
setField(transport, "connector", connector);
@@ -90,10 +84,10 @@ class HttpBackendProxyTransportTest {
@Test
void closedPublicationGateRejectsDeliveryForReplay() {
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
transport.close();
assertThrows(IllegalStateException.class, () -> HttpBackendProxyTransportAccess.dispatchAfterPublication(transport,
assertThrows(IllegalStateException.class, () -> transport.dispatchAfterPublication(
mock(GlobalMessageHandler.class), JsonEnvelope.builder("test").build()));
}
@@ -104,7 +98,7 @@ class HttpBackendProxyTransportTest {
when(connector.send(pending)).thenReturn(false, true);
when(connector.flushOutgoing(org.mockito.ArgumentMatchers.anyLong())).thenReturn(true);
assertTrue(HttpBackendProxyTransportAccess.flushHandoffForShutdown(connector, List.of(pending),
assertTrue(HttpBackendProxyTransport.flushHandoffForShutdown(connector, List.of(pending),
System.nanoTime() + TimeUnit.SECONDS.toNanos(1)));
org.mockito.InOrder order = org.mockito.Mockito.inOrder(connector);
@@ -115,7 +109,7 @@ class HttpBackendProxyTransportTest {
@Test
@SuppressWarnings("unchecked")
void unpublishedHandoffMessagesAreDiscardedOnRollback() throws Exception {
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
setField(transport, "connector", connector);
((java.util.ArrayDeque<JsonEnvelope>) field(transport, "handoffQueue"))
@@ -136,7 +130,7 @@ class HttpBackendProxyTransportTest {
when(connector.send(pending)).thenReturn(false);
assertThrows(IllegalStateException.class,
() -> HttpBackendProxyTransportAccess.transferStartupQueue(queue, connector));
() -> HttpBackendProxyTransport.transferStartupQueue(queue, connector));
assertEquals(java.util.List.of(pending), new java.util.ArrayList<>(queue));
}
@@ -148,7 +142,7 @@ class HttpBackendProxyTransportTest {
CountDownLatch completion = new CountDownLatch(1);
assertThrows(IllegalStateException.class,
() -> HttpBackendProxyTransportAccess.finishInitialization(false, connector, true, owner, completion));
() -> HttpBackendProxyTransport.finishInitialization(false, connector, true, owner, completion));
verify(connector).close();
assertEquals(1, owner.availablePermits());
@@ -158,7 +152,7 @@ class HttpBackendProxyTransportTest {
@Test
@SuppressWarnings("unchecked")
void orderlyShutdownFlushesRecoveryQueueBeforeHandoffQueue() throws Exception {
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
JsonEnvelope recovery = JsonEnvelope.builder("recovery").build();
JsonEnvelope handoff = JsonEnvelope.builder("handoff").build();
@@ -187,8 +181,8 @@ class HttpBackendProxyTransportTest {
setField(manager, "transport", failed);
org.mockito.Mockito.doThrow(new IllegalStateException("worker still stopping"))
.when(failed).prepareForReplacement();
when(HttpBackendProxyTransportAccess.isClosedForReplacement(failed)).thenReturn(true);
when(HttpBackendProxyTransportAccess.recreatePrepared(failed)).thenReturn(restored);
when(failed.isClosedForReplacement()).thenReturn(true);
when(failed.recreatePrepared()).thenReturn(restored);
assertThrows(IllegalStateException.class, manager::prepareForReplacement);
manager.send(queued);
@@ -209,7 +203,7 @@ class HttpBackendProxyTransportTest {
manager.send(queued);
verify(active).send(queued);
HttpBackendProxyTransportAccess.recreatePrepared(verify(active, org.mockito.Mockito.never()));
verify(active, org.mockito.Mockito.never()).recreatePrepared();
}
@Test
@@ -273,7 +267,7 @@ class HttpBackendProxyTransportTest {
});
assertThrows(IllegalStateException.class,
() -> HttpBackendProxyTransportAccess.dispatchIncoming(new HttpBackendProxyTransport(plugin), handler,
() -> new HttpBackendProxyTransport(plugin).dispatchIncoming(handler,
JsonEnvelope.builder("test").build(), System.nanoTime()));
assertTrue(scheduled.get() != null);
scheduled.get().run();
@@ -300,7 +294,7 @@ class HttpBackendProxyTransportTest {
AtomicReference<Throwable> dispatchFailure = new AtomicReference<>();
Thread dispatch = new Thread(() -> {
try {
HttpBackendProxyTransportAccess.dispatchIncoming(new HttpBackendProxyTransport(plugin), handler,
new HttpBackendProxyTransport(plugin).dispatchIncoming(handler,
JsonEnvelope.builder("test").build(), System.nanoTime() + TimeUnit.SECONDS.toNanos(5));
} catch (Throwable thrown) {
dispatchFailure.set(thrown);
@@ -323,28 +317,28 @@ class HttpBackendProxyTransportTest {
when(plugin.getDataFolder()).thenReturn(directory.toFile());
when(plugin.getBungeeSettings()).thenReturn(settings);
when(settings.getServer()).thenReturn("lobby-1");
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(plugin);
when(settings.getHttpConnectionCode()).thenReturn("malformed");
assertThrows(IllegalStateException.class, () -> HttpBackendProxyTransportAccess.validate(transport));
assertThrows(IllegalStateException.class, transport::validate);
when(settings.getHttpConnectionCode()).thenReturn(code("lobby-1", Instant.now().minusSeconds(1)).encode());
assertThrows(IllegalStateException.class, () -> HttpBackendProxyTransportAccess.validate(transport));
assertThrows(IllegalStateException.class, transport::validate);
when(settings.getHttpConnectionCode()).thenReturn(code("survival", Instant.now().plusSeconds(60)).encode());
assertThrows(IllegalStateException.class, () -> HttpBackendProxyTransportAccess.validate(transport));
assertThrows(IllegalStateException.class, transport::validate);
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(() -> HttpBackendProxyTransportAccess.validate(transport));
assertDoesNotThrow(() -> transport.validate());
}
@Test
void ordinaryStartupRetriesTransientInitialEnrollmentWithBoundedBackoff() throws Exception {
VotingPluginMain plugin = mock(VotingPluginMain.class);
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(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();
@@ -355,7 +349,7 @@ class HttpBackendProxyTransportTest {
if (attempts.getAndIncrement() == 0) throw new IOException("proxy unavailable");
return null;
});
assertTrue(HttpBackendProxyTransportAccess.enrollForStartup(transport, enrollment, "lobby-1", directory, true,
assertTrue(transport.enrollForStartup(enrollment, "lobby-1", directory, true,
delay -> { delays.add(delay); return true; }));
}
@@ -365,11 +359,11 @@ class HttpBackendProxyTransportTest {
@Test
void replacementCancelsRetryingInitialEnrollmentWithoutCredential() throws Exception {
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
Path credentials = directory.resolve("http");
java.lang.reflect.Field directoryField = HttpBackendProxyTransport.class.getDeclaredField("configuredDirectory");
directoryField.setAccessible(true);
ReflectiveAccess.set(directoryField, transport, credentials);
directoryField.set(transport, credentials);
java.lang.reflect.Field ownersField = HttpBackendProxyTransport.class.getDeclaredField("DIRECTORY_OWNERS");
ownersField.setAccessible(true);
@SuppressWarnings("unchecked")
@@ -386,7 +380,7 @@ class HttpBackendProxyTransportTest {
});
java.lang.reflect.Field workerField = HttpBackendProxyTransport.class.getDeclaredField("worker");
workerField.setAccessible(true);
ReflectiveAccess.set(workerField, transport, retryingEnrollment);
workerField.set(transport, retryingEnrollment);
retryingEnrollment.start();
assertTrue(workerStarted.await(1, TimeUnit.SECONDS));
JsonEnvelope queued = JsonEnvelope.builder("queued-before-replacement").build();
@@ -398,7 +392,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), HttpBackendProxyTransportAccess.drainPreparedMessages(transport),
assertEquals(List.of(queued), transport.drainPreparedMessages(),
"messages accepted before replacement must remain available for handoff");
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
@@ -410,7 +404,7 @@ class HttpBackendProxyTransportTest {
@Test
void replacementSnapshotsInitialEnrollmentPublishedWhileCancellationCompletes() throws Exception {
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
Path credentials = directory.resolve("http");
HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("original-proxy"),
"proxy.example.test");
@@ -457,7 +451,7 @@ class HttpBackendProxyTransportTest {
assertTrue(HttpClientCredentialStore.hasEnrolledProfile(credentials));
HttpConnectionCode original = code("lobby-1", Instant.now().plusSeconds(60));
HttpBackendProxyTransportAccess.restoreUnenrolledCredentialState(credentials, "lobby-1", original.encode());
HttpBackendProxyTransport.restoreUnenrolledCredentialState(credentials, "lobby-1", original.encode());
assertFalse(HttpClientCredentialStore.hasEnrolledProfile(credentials),
"a failed staged first enrollment must not remain active during rollback");
@@ -472,7 +466,7 @@ class HttpBackendProxyTransportTest {
identity.serverCertificatePin(), identity.caCertificatePin(), Instant.now().plusSeconds(60), "F".repeat(43));
HttpClientCredentialStore.saveEnrolled(credentials, original, identity.issueClientCertificate("lobby-1"));
HttpBackendProxyTransportAccess.restoreUnenrolledCredentialState(credentials, "lobby-1", original.encode());
HttpBackendProxyTransport.restoreUnenrolledCredentialState(credentials, "lobby-1", original.encode());
assertTrue(HttpClientCredentialStore.hasEnrolledProfile(credentials),
"a credential issued for the consumed original code must remain recoverable");
@@ -482,13 +476,13 @@ class HttpBackendProxyTransportTest {
void stagedEnrollmentRemainsFailFast() throws Exception {
VotingPluginMain plugin = mock(VotingPluginMain.class);
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(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,
() -> HttpBackendProxyTransportAccess.enrollForStartup(transport, enrollment, "lobby-1", directory, false,
() -> transport.enrollForStartup(enrollment, "lobby-1", directory, false,
delay -> { throw new AssertionError("staged enrollment must not retry"); }));
}
}
@@ -500,32 +494,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(HttpBackendProxyTransportAccess.enrollmentCode(credentials, "lobby-1", ""));
assertNull(HttpBackendProxyTransportAccess.enrollmentCode(credentials, "lobby-1", original.encode()),
assertNull(HttpBackendProxyTransport.enrollmentCode(credentials, "lobby-1", ""));
assertNull(HttpBackendProxyTransport.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(), HttpBackendProxyTransportAccess.enrollmentCode(credentials, "lobby-1", replacement.encode()).encode());
assertEquals(replacement.encode(), HttpBackendProxyTransport.enrollmentCode(credentials, "lobby-1", replacement.encode()).encode());
assertThrows(IllegalStateException.class,
() -> HttpBackendProxyTransportAccess.enrollmentCode(credentials, "lobby-1", "malformed"));
() -> HttpBackendProxyTransport.enrollmentCode(credentials, "lobby-1", "malformed"));
}
@Test
void failedReplacementFlushResumesTheExistingConnector() {
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
when(connector.flushOutgoing(org.mockito.ArgumentMatchers.anyLong())).thenReturn(false);
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
assertThrows(IllegalStateException.class,
() -> HttpBackendProxyTransportAccess.flushForReplacement(transport, connector, System.nanoTime()));
() -> transport.flushForReplacement(connector, System.nanoTime()));
verify(connector).start();
}
@Test
void closeNeverWaitsForSetupOnTheCallingThread() throws Exception {
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
CountDownLatch started = new CountDownLatch(1), release = new CountDownLatch(1);
Thread blocked = new Thread(() -> {
started.countDown();
@@ -536,7 +530,7 @@ class HttpBackendProxyTransportTest {
assertTrue(started.await(1, TimeUnit.SECONDS));
java.lang.reflect.Field worker = HttpBackendProxyTransport.class.getDeclaredField("worker");
worker.setAccessible(true);
ReflectiveAccess.set(worker, transport, blocked);
worker.set(transport, blocked);
long startedAt = System.nanoTime();
transport.close();
@@ -547,7 +541,7 @@ class HttpBackendProxyTransportTest {
@Test
void prepareForReplacementSnapshotsThePersistedCredentialGeneration() throws Exception {
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(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/"),
@@ -557,12 +551,12 @@ class HttpBackendProxyTransportTest {
HttpClientCredentialStore.snapshotActiveGeneration(credentials);
java.lang.reflect.Field directoryField = HttpBackendProxyTransport.class.getDeclaredField("configuredDirectory");
directoryField.setAccessible(true);
ReflectiveAccess.set(directoryField, transport, credentials);
directoryField.set(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);
ReflectiveAccess.set(connectorField, transport, connector);
connectorField.set(transport, connector);
assertDoesNotThrow(transport::prepareForReplacement);
java.lang.reflect.Field generationField =
@@ -591,12 +585,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 = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(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 { HttpBackendProxyTransportAccess.validate(transport, System.nanoTime() + TimeUnit.SECONDS.toNanos(1)); }
try { transport.validate(System.nanoTime() + TimeUnit.SECONDS.toNanos(1)); }
catch (Throwable thrown) { failure.set(thrown); }
finally { finished.countDown(); }
});
@@ -617,32 +611,32 @@ class HttpBackendProxyTransportTest {
void preparedHandoffMessagesStayAheadOfNewlyPublishedSendsWhenConnectorIsFull() throws Exception {
VotingPluginMain plugin = mock(VotingPluginMain.class);
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(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();
HttpBackendProxyTransportAccess.beginPreparedHandoff(transport);
transport.beginPreparedHandoff();
transport.send(sentAfterPublication);
HttpBackendProxyTransportAccess.acceptHandoffMessages(transport, List.of(acceptedBeforePublication));
transport.acceptHandoffMessages(List.of(acceptedBeforePublication));
assertEquals(List.of(acceptedBeforePublication, sentAfterPublication), HttpBackendProxyTransportAccess.handoffMessagesSnapshot(transport));
assertEquals(List.of(acceptedBeforePublication, sentAfterPublication), transport.handoffMessagesSnapshot());
transport.close();
}
@Test
@SuppressWarnings("unchecked")
void shutdownManagerRemainderStaysBehindAlreadyTransferredPrefix() throws Exception {
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(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);
HttpBackendProxyTransportAccess.appendShutdownHandoffMessages(transport, List.of(newerRemainder));
transport.appendShutdownHandoffMessages(List.of(newerRemainder));
assertEquals(List.of(olderPrefix, newerRemainder), HttpBackendProxyTransportAccess.handoffMessagesSnapshot(transport));
assertEquals(List.of(olderPrefix, newerRemainder), transport.handoffMessagesSnapshot());
transport.close();
}
@@ -653,8 +647,8 @@ class HttpBackendProxyTransportTest {
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
BackendProxyTransportManager previous = new BackendProxyTransportManager(plugin);
BackendProxyTransportManager replacement = new BackendProxyTransportManager(plugin);
HttpBackendProxyTransport oldTransport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport newTransport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport oldTransport = new HttpBackendProxyTransport(plugin);
HttpBackendProxyTransport newTransport = new HttpBackendProxyTransport(plugin);
setField(previous, "preparedTransport", oldTransport);
setField(replacement, "transport", newTransport);
java.util.ArrayDeque<JsonEnvelope> oldQueue =
@@ -683,12 +677,12 @@ class HttpBackendProxyTransportTest {
assertDoesNotThrow(() -> previous.completePreparedTransportHandoff(replacement));
replacement.awaitAsyncHandoff(System.nanoTime() + TimeUnit.SECONDS.toNanos(2));
List<JsonEnvelope> queued = HttpBackendProxyTransportAccess.handoffMessagesSnapshot(newTransport);
List<JsonEnvelope> queued = newTransport.handoffMessagesSnapshot();
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, HttpBackendProxyTransportAccess.preparedMessageCount(oldTransport));
assertEquals(0, oldTransport.preparedMessageCount());
assertTrue(((java.util.ArrayDeque<JsonEnvelope>) field(previous, "preparedSends")).isEmpty());
newTransport.close();
}
@@ -699,8 +693,8 @@ class HttpBackendProxyTransportTest {
VotingPluginMain plugin = mock(VotingPluginMain.class);
BackendProxyTransportManager previous = new BackendProxyTransportManager(plugin);
BackendProxyTransportManager replacement = new BackendProxyTransportManager(plugin);
HttpBackendProxyTransport oldTransport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport newTransport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport oldTransport = new HttpBackendProxyTransport(plugin);
HttpBackendProxyTransport newTransport = new HttpBackendProxyTransport(plugin);
setField(previous, "preparedTransport", oldTransport);
setField(replacement, "transport", newTransport);
java.util.ArrayDeque<JsonEnvelope> oldQueue =
@@ -724,8 +718,8 @@ class HttpBackendProxyTransportTest {
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
BackendProxyTransportManager previous = new BackendProxyTransportManager(plugin);
BackendProxyTransportManager replacement = new BackendProxyTransportManager(plugin);
HttpBackendProxyTransport oldTransport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport newTransport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport oldTransport = new HttpBackendProxyTransport(plugin);
HttpBackendProxyTransport newTransport = new HttpBackendProxyTransport(plugin);
setField(previous, "preparedTransport", oldTransport);
setField(replacement, "transport", newTransport);
java.util.ArrayDeque<JsonEnvelope> oldQueue =
@@ -745,7 +739,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(HttpBackendProxyTransportAccess.handoffMessagesSnapshot(newTransport).isEmpty());
assertTrue(newTransport.handoffMessagesSnapshot().isEmpty());
}
@Test
@@ -754,7 +748,7 @@ class HttpBackendProxyTransportTest {
VotingPluginMain plugin = mock(VotingPluginMain.class);
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
BackendProxyTransportManager manager = new BackendProxyTransportManager(plugin);
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(plugin);
setField(manager, "transport", transport);
setField(transport, "managerHandoffActive", true);
java.util.ArrayDeque<JsonEnvelope> httpQueue =
@@ -778,7 +772,7 @@ class HttpBackendProxyTransportTest {
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
BackendProxyTransportManager previous = new BackendProxyTransportManager(plugin);
BackendProxyTransportManager replacement = new BackendProxyTransportManager(plugin);
HttpBackendProxyTransport oldTransport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport oldTransport = new HttpBackendProxyTransport(plugin);
HttpBackendProxyTransport newTransport = mock(HttpBackendProxyTransport.class);
JsonEnvelope envelope = JsonEnvelope.builder("accepted-once").build();
CountDownLatch sending = new CountDownLatch(1);
@@ -807,7 +801,8 @@ class HttpBackendProxyTransportTest {
closing.join(TimeUnit.SECONDS.toMillis(2));
assertFalse(closing.isAlive());
verify(newTransport).send(envelope);
HttpBackendProxyTransportAccess.acceptHandoffMessages(verify(newTransport, org.mockito.Mockito.never()), org.mockito.ArgumentMatchers.anyList());
verify(newTransport, org.mockito.Mockito.never())
.acceptHandoffMessages(org.mockito.ArgumentMatchers.anyList());
}
@Test
@@ -817,8 +812,8 @@ class HttpBackendProxyTransportTest {
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
BackendProxyTransportManager previous = new BackendProxyTransportManager(plugin);
BackendProxyTransportManager replacement = new BackendProxyTransportManager(plugin);
HttpBackendProxyTransport oldTransport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport newTransport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport oldTransport = new HttpBackendProxyTransport(plugin);
HttpBackendProxyTransport newTransport = new HttpBackendProxyTransport(plugin);
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
JsonEnvelope source = JsonEnvelope.builder("source").build();
JsonEnvelope adapted = JsonEnvelope.builder("adapted").build();
@@ -879,7 +874,7 @@ class HttpBackendProxyTransportTest {
VotingPluginMain plugin = mock(VotingPluginMain.class);
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
BackendProxyTransportManager manager = new BackendProxyTransportManager(plugin);
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(plugin);
HttpBackendTransportConnector connector = mock(HttpBackendTransportConnector.class);
JsonEnvelope envelope = JsonEnvelope.builder("shutdown").build();
CountDownLatch sending = new CountDownLatch(1);
@@ -946,8 +941,8 @@ class HttpBackendProxyTransportTest {
VotingPluginMain plugin = mock(VotingPluginMain.class);
BackendProxyTransportManager previous = new BackendProxyTransportManager(plugin);
BackendProxyTransportManager replacement = new BackendProxyTransportManager(plugin);
HttpBackendProxyTransport oldTransport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport newTransport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport oldTransport = new HttpBackendProxyTransport(plugin);
HttpBackendProxyTransport newTransport = new HttpBackendProxyTransport(plugin);
JsonEnvelope old = JsonEnvelope.builder("old").build();
JsonEnvelope prepared = JsonEnvelope.builder("prepared").build();
setField(previous, "preparedTransport", oldTransport);
@@ -961,7 +956,7 @@ class HttpBackendProxyTransportTest {
assertThrows(IllegalStateException.class, () -> previous.completePreparedTransportHandoff(replacement));
assertEquals(List.of(old), HttpBackendProxyTransportAccess.preparedMessagesSnapshot(oldTransport));
assertEquals(List.of(old), oldTransport.preparedMessagesSnapshot());
java.util.ArrayDeque<?> retained = (java.util.ArrayDeque<?>) field(previous, "preparedSends");
assertEquals(1, retained.size());
assertSame(prepared, field(retained.peekFirst(), "source"));
@@ -971,13 +966,13 @@ class HttpBackendProxyTransportTest {
@Test
@SuppressWarnings("unchecked")
void preparedDrainPreservesStartupAndExistingHandoffMessagesInFifoOrder() throws Exception {
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(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), HttpBackendProxyTransportAccess.drainPreparedMessages(transport));
assertEquals(List.of(startup, handoff), transport.drainPreparedMessages());
assertTrue(((java.util.ArrayDeque<JsonEnvelope>) field(transport, "startupQueue")).isEmpty());
assertTrue(((java.util.ArrayDeque<JsonEnvelope>) field(transport, "handoffQueue")).isEmpty());
}
@@ -987,7 +982,7 @@ class HttpBackendProxyTransportTest {
void rollbackRecreateKeepsAnOversizedCombinedQueueInTheDrainedHandoffLane() throws Exception {
VotingPluginMain plugin = mock(VotingPluginMain.class);
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(plugin);
Path credentials = directory.resolve("http");
setField(transport, "configuredDirectory", credentials);
setField(transport, "configuredServerId", "lobby-1");
@@ -1014,7 +1009,7 @@ class HttpBackendProxyTransportTest {
HttpBackendProxyTransport restored = null;
Thread setup = null;
try {
restored = HttpBackendProxyTransportAccess.recreatePrepared(transport);
restored = transport.recreatePrepared();
assertTrue(((java.util.ArrayDeque<JsonEnvelope>) field(restored, "startupQueue")).isEmpty(),
"rollback must not synchronously overfill the connector's 1024-entry queue");
assertEquals(queued,
@@ -1036,7 +1031,7 @@ class HttpBackendProxyTransportTest {
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getAnonymousLogger());
BackendProxyTransportManager manager = new BackendProxyTransportManager(plugin);
HttpBackendProxyTransport previous = mock(HttpBackendProxyTransport.class);
HttpBackendProxyTransport restored = HttpBackendProxyTransportAccess.create(plugin);
HttpBackendProxyTransport restored = new HttpBackendProxyTransport(plugin);
java.util.ArrayDeque<JsonEnvelope> restoredQueue =
(java.util.ArrayDeque<JsonEnvelope>) field(restored, "handoffQueue");
for (int index = 0; index < 4096; index++)
@@ -1044,29 +1039,29 @@ class HttpBackendProxyTransportTest {
JsonEnvelope first = JsonEnvelope.builder("fenced-1").build();
JsonEnvelope second = JsonEnvelope.builder("fenced-2").build();
setField(manager, "preparedTransport", previous);
when(HttpBackendProxyTransportAccess.recreatePrepared(previous)).thenReturn(restored);
when(previous.recreatePrepared()).thenReturn(restored);
manager.send(first);
manager.send(second);
manager.restorePreparedTransport();
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
while (HttpBackendProxyTransportAccess.handoffMessagesSnapshot(restored).size() != 4098 && System.nanoTime() < deadline)
while (restored.handoffMessagesSnapshot().size() != 4098 && System.nanoTime() < deadline)
Thread.onSpinWait();
assertEquals(4098, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(restored).size());
assertSame(first, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(restored).get(4096));
assertSame(second, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(restored).get(4097));
assertEquals(4098, restored.handoffMessagesSnapshot().size());
assertSame(first, restored.handoffMessagesSnapshot().get(4096));
assertSame(second, restored.handoffMessagesSnapshot().get(4097));
assertTrue(((java.util.ArrayDeque<?>) field(manager, "asyncHandoffSends")).isEmpty());
manager.send(JsonEnvelope.builder("newer-rejected").build());
assertEquals(4098, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(restored).size(),
assertEquals(4098, restored.handoffMessagesSnapshot().size(),
"normal sends must not expand the one-time rollback recovery allowance");
}
@Test
@SuppressWarnings("unchecked")
void shutdownCanAppendTheReservedRollbackRemainder() throws Exception {
HttpBackendProxyTransport transport = HttpBackendProxyTransportAccess.create(mock(VotingPluginMain.class));
HttpBackendProxyTransport transport = new HttpBackendProxyTransport(mock(VotingPluginMain.class));
java.util.ArrayDeque<JsonEnvelope> handoff =
(java.util.ArrayDeque<JsonEnvelope>) field(transport, "handoffQueue");
for (int index = 0; index < 4096; index++)
@@ -1074,12 +1069,12 @@ class HttpBackendProxyTransportTest {
JsonEnvelope first = JsonEnvelope.builder("fenced-1").build();
JsonEnvelope second = JsonEnvelope.builder("fenced-2").build();
HttpBackendProxyTransportAccess.activateRestoredHandoffDrain(transport, 2);
HttpBackendProxyTransportAccess.appendShutdownHandoffMessages(transport, List.of(first, second));
transport.activateRestoredHandoffDrain(2);
transport.appendShutdownHandoffMessages(List.of(first, second));
assertEquals(4098, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(transport).size());
assertSame(first, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(transport).get(4096));
assertSame(second, HttpBackendProxyTransportAccess.handoffMessagesSnapshot(transport).get(4097));
assertEquals(4098, transport.handoffMessagesSnapshot().size());
assertSame(first, transport.handoffMessagesSnapshot().get(4096));
assertSame(second, transport.handoffMessagesSnapshot().get(4097));
}
private static HttpConnectionCode code(String serverId, Instant expiry) {
@@ -1090,7 +1085,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);
ReflectiveAccess.set(field, target, value);
field.set(target, value);
}
private static Object field(Object target, String name) throws Exception {
@@ -1,8 +1,4 @@
package com.bencodez.votingplugin.tests.backendproxy.transport;
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
import com.bencodez.votingplugin.backendproxy.transport.MqttBackendProxyTransport;
package com.bencodez.votingplugin.backendproxy.transport;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -38,12 +34,12 @@ class MqttBackendProxyTransportTest {
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
setField(transport, "messageHandler", messages);
setField(transport, "authenticator", authenticator);
MqttBackendProxyTransportAccess.updateSecurity(transport, authenticator, encryption);
transport.updateSecurity(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");
MqttBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "votingplugin/servers/backend-a");
transport.acceptAuthenticatedEnvelope(signed, "votingplugin/servers/backend-a");
verify(messages).onMessage(org.mockito.ArgumentMatchers.argThat(received ->
VotingPluginWire.SUB_VOTE.equals(received.getSubChannel())
@@ -59,9 +55,9 @@ class MqttBackendProxyTransportTest {
setField(transport, "authenticator", authenticator);
JsonEnvelope vote = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE).put("player", "Alex").build();
MqttBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, authenticator.sign(vote, Domain.MQTT_PROXY_BACKEND, "proxy-a", "votingplugin/servers/backend-a"),
transport.acceptAuthenticatedEnvelope(authenticator.sign(vote, Domain.MQTT_PROXY_BACKEND, "proxy-a", "votingplugin/servers/backend-a"),
"votingplugin/servers/backend-a");
MqttBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, vote, "votingplugin/servers/backend-a");
transport.acceptAuthenticatedEnvelope(vote, "votingplugin/servers/backend-a");
verify(messages).onMessage(org.mockito.ArgumentMatchers.argThat(received ->
VotingPluginWire.SUB_VOTE.equals(received.getSubChannel())
@@ -80,9 +76,9 @@ class MqttBackendProxyTransportTest {
JsonEnvelope signed = authenticator.sign(vote, Domain.MQTT_PROXY_BACKEND, "proxy-a",
"votingplugin/servers/backend-a");
MqttBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "votingplugin/servers/backend-b");
transport.acceptAuthenticatedEnvelope(signed, "votingplugin/servers/backend-b");
org.mockito.Mockito.verifyNoInteractions(messages);
MqttBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "votingplugin/servers/backend-a");
transport.acceptAuthenticatedEnvelope(signed, "votingplugin/servers/backend-a");
verify(messages).onMessage(org.mockito.ArgumentMatchers.any());
}
@@ -96,6 +92,6 @@ class MqttBackendProxyTransportTest {
private static void setField(Object target, String name, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(name);
field.setAccessible(true);
ReflectiveAccess.set(field, target, value);
field.set(target, value);
}
}
@@ -1,10 +1,4 @@
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;
package com.bencodez.votingplugin.backendproxy.transport;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -56,17 +50,17 @@ class RedisBackendProxyTransportTest {
SharedTransportEnvelopeAuthenticator authenticator = authenticator();
TransportEnvelopeEncryption encryption = TransportEnvelopeEncryption.load(
temporaryDirectory.resolve("secretkey.key"), TransportEnvelopeEncryption.Domain.PROXY_BACKEND, true);
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
setField(transport, "messageHandler", messages);
setField(transport, "authenticator", authenticator);
RedisBackendProxyTransportAccess.updateSecurity(transport, authenticator, encryption);
transport.updateSecurity(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");
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "VotingPlugin_backend-a");
transport.acceptAuthenticatedEnvelope(signed, "VotingPlugin_backend-a");
verify(messages).onMessage(org.mockito.ArgumentMatchers.argThat(received ->
VotingPluginWire.SUB_VOTE.equals(received.getSubChannel())
@@ -76,7 +70,7 @@ class RedisBackendProxyTransportTest {
@Test
void authenticatedVoteIsAcceptedAndUnsignedPresenceIsRejectedBeforeDispatch() throws Exception {
SharedTransportEnvelopeAuthenticator authenticator = authenticator();
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
setField(transport, "messageHandler", messages);
setField(transport, "authenticator", authenticator);
@@ -85,9 +79,9 @@ class RedisBackendProxyTransportTest {
JsonEnvelope signedVote = authenticator.sign(VotingPluginWire.withRedisDeliveryId(vote),
Domain.REDIS_PROXY_BACKEND, "proxy-a", "VotingPlugin_backend-a");
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signedVote, "VotingPlugin_backend-a");
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, vote, "VotingPlugin_backend-a");
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, VotingPluginWire.login("Alex",
transport.acceptAuthenticatedEnvelope(signedVote, "VotingPlugin_backend-a");
transport.acceptAuthenticatedEnvelope(vote, "VotingPlugin_backend-a");
transport.acceptAuthenticatedEnvelope(VotingPluginWire.login("Alex",
"00000000-0000-0000-0000-000000000001", "backend-a"), "VotingPlugin_backend-a");
verify(messages).onMessage(org.mockito.ArgumentMatchers.argThat(received ->
@@ -100,7 +94,7 @@ class RedisBackendProxyTransportTest {
@Test
void copiedRedisVoteCannotAuthenticateOnAnotherBackendChannel() throws Exception {
SharedTransportEnvelopeAuthenticator authenticator = authenticator();
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
setField(transport, "messageHandler", messages);
setField(transport, "authenticator", authenticator);
@@ -108,31 +102,31 @@ class RedisBackendProxyTransportTest {
JsonEnvelope signed = authenticator.sign(vote, Domain.REDIS_PROXY_BACKEND, "proxy-a",
"VotingPlugin_backend-a");
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "VotingPlugin_backend-b");
transport.acceptAuthenticatedEnvelope(signed, "VotingPlugin_backend-b");
verifyNoInteractions(messages);
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "VotingPlugin_backend-a");
transport.acceptAuthenticatedEnvelope(signed, "VotingPlugin_backend-a");
verify(messages).onMessage(org.mockito.ArgumentMatchers.any());
}
@Test
void exactAuthenticatedRedisReplayCannotDuplicateVoteProcessing() throws Exception {
SharedTransportEnvelopeAuthenticator authenticator = authenticator();
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
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");
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "VotingPlugin_backend-a");
RedisBackendProxyTransportAccess.acceptAuthenticatedEnvelope(transport, signed, "VotingPlugin_backend-a");
transport.acceptAuthenticatedEnvelope(signed, "VotingPlugin_backend-a");
transport.acceptAuthenticatedEnvelope(signed, "VotingPlugin_backend-a");
verify(messages, times(1)).onMessage(org.mockito.ArgumentMatchers.any());
}
@Test
void validationHonorsTlsAndHostnameVerification() {
DefaultJedisClientConfig config = RedisBackendProxyTransportAccess.buildValidationClientConfig(3, "user", "secret",
DefaultJedisClientConfig config = RedisBackendProxyTransport.buildValidationClientConfig(3, "user", "secret",
true);
assertTrue(config.isSsl());
@@ -149,20 +143,20 @@ class RedisBackendProxyTransportTest {
@Test
void validationKeepsTlsDisabledByDefault() {
assertFalse(RedisBackendProxyTransportAccess.buildValidationClientConfig(0, null, null, false).isSsl());
assertFalse(RedisBackendProxyTransport.buildValidationClientConfig(0, null, null, false).isSsl());
}
@Test
void legacyHandoffBufferDegradesAtItsFixedLimit() throws Exception {
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
handler.setAccessible(true);
ReflectiveAccess.set(handler, transport, messages);
handler.set(transport, messages);
JsonEnvelope envelope = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build();
for (int index = 0; index <= RedisBackendProxyTransportAccess.MAX_LEGACY_HANDOFF_DELIVERIES; index++) {
RedisBackendProxyTransportAccess.dispatchLegacy(transport, envelope);
for (int index = 0; index <= RedisBackendProxyTransport.MAX_LEGACY_HANDOFF_DELIVERIES; index++) {
transport.dispatchLegacy(envelope);
}
verify(messages, times(1)).onMessage(envelope);
@@ -170,45 +164,45 @@ class RedisBackendProxyTransportTest {
@Test
void legacyHandoffBufferDegradesForAnOversizedEnvelope() throws Exception {
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
handler.setAccessible(true);
ReflectiveAccess.set(handler, transport, messages);
handler.set(transport, messages);
JsonEnvelope envelope = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE)
.put("payload", "x".repeat(ProcessedVoteCache.MAX_LEGACY_REDIS_DELIVERY_BYTES + 1)).build();
RedisBackendProxyTransportAccess.dispatchLegacy(transport, envelope);
transport.dispatchLegacy(envelope);
verify(messages).onMessage(envelope);
}
@Test
void retiredListenerDropsLegacyDeliveriesAfterShutdownTimeout() throws Exception {
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
handler.setAccessible(true);
ReflectiveAccess.set(handler, transport, messages);
RedisBackendProxyTransportAccess.fenceAfterHandoff(transport);
handler.set(transport, messages);
transport.fenceAfterHandoff();
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build());
transport.dispatchLegacy(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build());
verifyNoInteractions(messages);
}
@Test
void retiredListenerDoesNotReserveOrDispatchDeliveryIds() throws Exception {
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
ProcessedVoteCache cache = new ProcessedVoteCache();
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, cache);
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
handler.setAccessible(true);
ReflectiveAccess.set(handler, transport, messages);
RedisBackendProxyTransportAccess.fenceAfterHandoff(transport);
handler.set(transport, messages);
transport.fenceAfterHandoff();
String deliveryId = "00000000-0000-0000-0000-000000000001";
RedisBackendProxyTransportAccess.dispatchIdentified(transport, JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build(), deliveryId);
transport.dispatchIdentified(JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build(), deliveryId);
assertTrue(cache.reserveRedisDelivery(deliveryId), "retired listener must not consume shared deduplication state");
verifyNoInteractions(messages);
@@ -216,7 +210,7 @@ class RedisBackendProxyTransportTest {
@Test
void standbyBuffersIdentifiedDeliveriesUntilItIsPublished() throws Exception {
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
ProcessedVoteCache cache = new ProcessedVoteCache();
cache.registerRedisSubscriber(new Object());
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, cache);
Field identity = RedisBackendProxyTransport.class.getDeclaredField("subscriberIdentity");
@@ -228,12 +222,12 @@ class RedisBackendProxyTransportTest {
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
handler.setAccessible(true);
ReflectiveAccess.set(handler, transport, messages);
handler.set(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();
RedisBackendProxyTransportAccess.dispatchIdentified(transport, envelope, deliveryId);
transport.dispatchIdentified(envelope, deliveryId);
verifyNoInteractions(messages);
transport.activateAfterHandoff();
@@ -258,8 +252,8 @@ class RedisBackendProxyTransportTest {
JsonEnvelope identified = JsonEnvelope.builder("identified")
.put(VotingPluginWire.K_REDIS_DELIVERY_ID, deliveryId).build();
RedisBackendProxyTransportAccess.dispatchLegacy(transport, legacy);
RedisBackendProxyTransportAccess.dispatchIdentified(transport, identified, deliveryId);
transport.dispatchLegacy(legacy);
transport.dispatchIdentified(identified, deliveryId);
transport.activateAfterHandoff();
transport.replayAfterHandoffPublication();
@@ -270,7 +264,7 @@ class RedisBackendProxyTransportTest {
@Test
void legacyCopiesDoNotExposeReliableDeliveryIdentity() throws Exception {
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
ProcessedVoteCache cache = new ProcessedVoteCache();
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, cache);
Field identity = RedisBackendProxyTransport.class.getDeclaredField("subscriberIdentity");
identity.setAccessible(true);
@@ -281,7 +275,7 @@ class RedisBackendProxyTransportTest {
JsonEnvelope mixed = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE)
.put(VotingPluginWire.K_REDIS_DELIVERY_ID, deliveryId).put("payload", "legacy").build();
RedisBackendProxyTransportAccess.dispatchLegacy(transport, mixed);
transport.dispatchLegacy(mixed);
org.mockito.ArgumentCaptor<JsonEnvelope> captured = org.mockito.ArgumentCaptor.forClass(JsonEnvelope.class);
verify(messages).onMessage(captured.capture());
@@ -294,9 +288,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 <= RedisBackendProxyTransportAccess.MAX_IDENTIFIED_HANDOFF_DELIVERIES; index++) {
for (int index = 0; index <= RedisBackendProxyTransport.MAX_IDENTIFIED_HANDOFF_DELIVERIES; index++) {
String deliveryId = String.format("00000000-0000-0000-0000-%012d", index);
RedisBackendProxyTransportAccess.dispatchIdentified(transport, JsonEnvelope.builder("identified")
transport.dispatchIdentified(JsonEnvelope.builder("identified")
.put(VotingPluginWire.K_REDIS_DELIVERY_ID, deliveryId).build(), deliveryId);
}
@@ -312,7 +306,7 @@ class RedisBackendProxyTransportTest {
JsonEnvelope oversized = JsonEnvelope.builder("legacy")
.put("payload", "x".repeat(ProcessedVoteCache.MAX_LEGACY_REDIS_DELIVERY_BYTES + 1)).build();
RedisBackendProxyTransportAccess.dispatchLegacy(transport, oversized);
transport.dispatchLegacy(oversized);
verifyNoInteractions(messages);
assertThrows(IllegalStateException.class, transport::activateAfterHandoff);
@@ -325,8 +319,8 @@ class RedisBackendProxyTransportTest {
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
setField(transport, "messageHandler", messages);
for (int index = 0; index <= RedisBackendProxyTransportAccess.MAX_LEGACY_HANDOFF_DELIVERIES; index++)
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder("legacy-" + index).build());
for (int index = 0; index <= RedisBackendProxyTransport.MAX_LEGACY_HANDOFF_DELIVERIES; index++)
transport.dispatchLegacy(JsonEnvelope.builder("legacy-" + index).build());
verifyNoInteractions(messages);
assertThrows(IllegalStateException.class, transport::activateAfterHandoff);
@@ -334,7 +328,7 @@ class RedisBackendProxyTransportTest {
@Test
void activeLegacyAccountingOverflowAbortsLaggingStandbyPublication() throws Exception {
ProcessedVoteCache cache = ProcessedVoteCacheAccess.create();
ProcessedVoteCache cache = new ProcessedVoteCache();
Object previousIdentity = new Object();
cache.registerRedisSubscriber(previousIdentity);
RedisBackendProxyTransport replacement = new RedisBackendProxyTransport(null, cache);
@@ -352,7 +346,7 @@ class RedisBackendProxyTransportTest {
@Test
void fencingWaitsForAnIdentifiedMessageHandler() throws Exception {
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, ProcessedVoteCacheAccess.create());
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(null, new ProcessedVoteCache());
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
GlobalMessageHandler messages = mock(GlobalMessageHandler.class);
@@ -367,8 +361,8 @@ class RedisBackendProxyTransportTest {
}).when(messages).onMessage(org.mockito.ArgumentMatchers.any(JsonEnvelope.class));
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
handler.setAccessible(true);
ReflectiveAccess.set(handler, transport, messages);
Thread delivery = new Thread(() -> RedisBackendProxyTransportAccess.dispatchIdentified(transport,
handler.set(transport, messages);
Thread delivery = new Thread(() -> transport.dispatchIdentified(
JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build(), "delivery"));
delivery.start();
assertTrue(entered.await(1, TimeUnit.SECONDS));
@@ -378,7 +372,7 @@ class RedisBackendProxyTransportTest {
new java.util.concurrent.atomic.AtomicReference<>();
Thread retirement = new Thread(() -> {
try {
RedisBackendProxyTransportAccess.fenceAfterHandoff(transport);
transport.fenceAfterHandoff();
} catch (Throwable failure) {
fenceFailure.set(failure);
} finally {
@@ -422,7 +416,7 @@ class RedisBackendProxyTransportTest {
}).when(messages).onMessage(org.mockito.ArgumentMatchers.any(JsonEnvelope.class));
setField(transport, "messageHandler", messages);
Thread delivery = new Thread(() -> RedisBackendProxyTransportAccess.dispatchLegacy(transport,
Thread delivery = new Thread(() -> transport.dispatchLegacy(
JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build()));
delivery.start();
assertTrue(callbackEntered.await(1, TimeUnit.SECONDS));
@@ -481,8 +475,8 @@ class RedisBackendProxyTransportTest {
JsonEnvelope identified = JsonEnvelope.builder("identified-overlap").build();
setBoolean(standby, "standbySubscriber", true);
setField(standby, "messageHandler", mock(GlobalMessageHandler.class));
RedisBackendProxyTransportAccess.dispatchLegacy(standby, legacy);
RedisBackendProxyTransportAccess.dispatchIdentified(standby, identified, "00000000-0000-0000-0000-000000001594");
standby.dispatchLegacy(legacy);
standby.dispatchIdentified(identified, "00000000-0000-0000-0000-000000001594");
org.mockito.Mockito.doThrow(new IllegalStateException("standby promotion rejected"))
.when(standby).activateAfterHandoff();
setField(previous, "transport", oldTransport);
@@ -539,13 +533,13 @@ class RedisBackendProxyTransportTest {
setField(replacement, "messageHandler", replacementMessages);
JsonEnvelope envelope = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE_UPDATE).build();
Thread delivery = new Thread(() -> RedisBackendProxyTransportAccess.dispatchLegacy(previous, envelope));
Thread delivery = new Thread(() -> previous.dispatchLegacy(envelope));
delivery.start();
assertTrue(entered.await(1, TimeUnit.SECONDS));
RedisBackendProxyTransportAccess.dispatchLegacy(replacement, envelope);
replacement.dispatchLegacy(envelope);
CountDownLatch promoted = new CountDownLatch(1);
Thread promotion = new Thread(() -> {
RedisBackendProxyTransportAccess.fenceAfterHandoff(previous);
previous.fenceAfterHandoff();
replacement.activateAfterHandoff();
promoted.countDown();
});
@@ -574,7 +568,7 @@ class RedisBackendProxyTransportTest {
JsonEnvelope envelope = invocation.getArgument(0);
order.add(envelope);
if (envelope == replayed) {
Thread arrival = new Thread(() -> RedisBackendProxyTransportAccess.dispatchIdentified(transport, newer, "new-delivery"));
Thread arrival = new Thread(() -> transport.dispatchIdentified(newer, "new-delivery"));
arrival.start();
arrival.join(TimeUnit.SECONDS.toMillis(1));
}
@@ -582,8 +576,8 @@ class RedisBackendProxyTransportTest {
}).when(messages).onMessage(org.mockito.ArgumentMatchers.any(JsonEnvelope.class));
Field handler = RedisBackendProxyTransport.class.getDeclaredField("messageHandler");
handler.setAccessible(true);
ReflectiveAccess.set(handler, transport, messages);
RedisBackendProxyTransportAccess.dispatchLegacy(transport, replayed);
handler.set(transport, messages);
transport.dispatchLegacy(replayed);
transport.activateAfterHandoff();
transport.replayAfterHandoffPublication();
@@ -607,15 +601,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(RedisBackendProxyTransportAccess.REPLAY_BATCH_SIZE + 1);
CountDownLatch replayed = new CountDownLatch(RedisBackendProxyTransport.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 <= RedisBackendProxyTransportAccess.REPLAY_BATCH_SIZE; index++)
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder("replay-" + index).build());
for (int index = 0; index <= RedisBackendProxyTransport.REPLAY_BATCH_SIZE; index++)
transport.dispatchLegacy(JsonEnvelope.builder("replay-" + index).build());
transport.activateAfterHandoff();
transport.replayAfterHandoffPublication();
@@ -636,14 +630,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 < RedisBackendProxyTransportAccess.MAX_REPLAY_HANDOFF_DELIVERIES; index++)
for (int index = 0; index < RedisBackendProxyTransport.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();
RedisBackendProxyTransportAccess.dispatchIdentified(transport, newer, "new-delivery");
transport.dispatchIdentified(newer, "new-delivery");
returned.countDown();
});
callback.start();
@@ -668,12 +662,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 < RedisBackendProxyTransportAccess.MAX_REPLAY_HANDOFF_DELIVERIES; index++)
for (int index = 0; index < RedisBackendProxyTransport.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(() -> {
RedisBackendProxyTransportAccess.dispatchReceivedSubscriberEnvelope(transport, received, "received-delivery");
transport.dispatchReceivedSubscriberEnvelope(received, "received-delivery");
completed.countDown();
});
subscriber.start();
@@ -706,11 +700,11 @@ class RedisBackendProxyTransportTest {
JsonEnvelope before = JsonEnvelope.builder("before").build();
JsonEnvelope during = JsonEnvelope.builder("during").build();
RedisBackendProxyTransportAccess.dispatchIdentified(replacement, before, "before-publication");
replacement.dispatchIdentified(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.
RedisBackendProxyTransportAccess.dispatchIdentified(replacement, during, "during-publication");
replacement.dispatchIdentified(during, "during-publication");
assertEquals(java.util.List.of(before, during), replacement.detachReplayForFailedHandoff());
assertTrue(replayQueue(replacement).isEmpty());
@@ -740,7 +734,7 @@ class RedisBackendProxyTransportTest {
setField(replacement, "messageHandler", messages);
replacement.acceptReplayFromPreviousHandoff(transferred);
JsonEnvelope newer = JsonEnvelope.builder("newer").build();
RedisBackendProxyTransportAccess.dispatchIdentified(replacement, newer, "new-overlap");
replacement.dispatchIdentified(newer, "new-overlap");
replacement.activateAfterHandoff();
replacement.replayAfterHandoffPublication();
@@ -760,7 +754,7 @@ class RedisBackendProxyTransportTest {
JsonEnvelope duringFreeze = JsonEnvelope.builder("during-freeze").build();
CountDownLatch completed = new CountDownLatch(1);
Thread callback = new Thread(() -> {
RedisBackendProxyTransportAccess.dispatchIdentified(previous, duringFreeze, "during-freeze");
previous.dispatchIdentified(duringFreeze, "during-freeze");
completed.countDown();
});
callback.start();
@@ -784,7 +778,7 @@ class RedisBackendProxyTransportTest {
previous.freezeReplayForSuccessiveHandoff();
CountDownLatch callbackDone = new CountDownLatch(1);
Thread callback = new Thread(() -> {
RedisBackendProxyTransportAccess.dispatchIdentified(previous, JsonEnvelope.builder( "during-close").build(), "during-close");
previous.dispatchIdentified(JsonEnvelope.builder("during-close").build(), "during-close");
callbackDone.countDown();
});
callback.start();
@@ -829,7 +823,7 @@ class RedisBackendProxyTransportTest {
@Test
void retiredRedisCleanupDoesNotBlockManagerCloseOnPublicationThread() throws Exception {
BackendProxyTransportManager manager = new BackendProxyTransportManager(null, ProcessedVoteCacheAccess.create());
BackendProxyTransportManager manager = new BackendProxyTransportManager(null, new ProcessedVoteCache());
RedisBackendProxyTransport retired = mock(RedisBackendProxyTransport.class);
CountDownLatch closeEntered = new CountDownLatch(1);
CountDownLatch releaseClose = new CountDownLatch(1);
@@ -866,7 +860,7 @@ class RedisBackendProxyTransportTest {
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(plugin, cache);
setBoolean(transport, "standbySubscriber", true);
setField(transport, "messageHandler", mock(GlobalMessageHandler.class));
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder("replay").build());
transport.dispatchLegacy(JsonEnvelope.builder("replay").build());
transport.activateAfterHandoff();
transport.replayAfterHandoffPublication();
@@ -895,7 +889,7 @@ class RedisBackendProxyTransportTest {
RedisBackendProxyTransport transport = new RedisBackendProxyTransport(plugin, cache);
setBoolean(transport, "standbySubscriber", true);
setField(transport, "messageHandler", mock(GlobalMessageHandler.class));
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder("replay").build());
transport.dispatchLegacy(JsonEnvelope.builder("replay").build());
transport.activateAfterHandoff();
transport.replayAfterHandoffPublication();
assertTrue(submitted.await(1, TimeUnit.SECONDS));
@@ -936,8 +930,8 @@ class RedisBackendProxyTransportTest {
return null;
}).when(messages).onMessage(org.mockito.ArgumentMatchers.any(JsonEnvelope.class));
setField(transport, "messageHandler", messages);
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder("first").build());
RedisBackendProxyTransportAccess.dispatchLegacy(transport, JsonEnvelope.builder("second").build());
transport.dispatchLegacy(JsonEnvelope.builder("first").build());
transport.dispatchLegacy(JsonEnvelope.builder("second").build());
transport.activateAfterHandoff();
transport.replayAfterHandoffPublication();
assertTrue(entered.await(1, TimeUnit.SECONDS));
@@ -967,7 +961,7 @@ class RedisBackendProxyTransportTest {
private static void setField(Object target, String name, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(name);
field.setAccessible(true);
ReflectiveAccess.set(field, target, value);
field.set(target, value);
}
private static java.util.List<JsonEnvelope> preparedSendQueue(BackendProxyTransportManager manager)
@@ -1,8 +1,4 @@
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;
package com.bencodez.votingplugin.backendproxy.transport;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -28,19 +24,19 @@ class SharedInboundPolicyTest {
SharedTransportEnvelopeAuthenticator required = SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED);
TransportEnvelopeEncryption encrypted = TransportEnvelopeEncryption.load(keyFile,
TransportEnvelopeEncryption.Domain.PROXY_BACKEND, true);
SharedInboundPolicyAccess policy = new SharedInboundPolicyAccess(RedisBackendProxyTransport.class, "vp:a", required,
SharedInboundPolicy policy = new SharedInboundPolicy(RedisBackendProxyTransport.class, "vp:a", required,
encrypted);
assertTrue(policy.hasEquivalentPolicy(new SharedInboundPolicyAccess(RedisBackendProxyTransport.class, "vp:a",
assertTrue(policy.hasEquivalentPolicy(new SharedInboundPolicy(RedisBackendProxyTransport.class, "vp:a",
SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED), TransportEnvelopeEncryption.load(keyFile,
TransportEnvelopeEncryption.Domain.PROXY_BACKEND, true))));
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicyAccess(RedisBackendProxyTransport.class, "vp:b",
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicy(RedisBackendProxyTransport.class, "vp:b",
SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED), encrypted)));
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicyAccess(MqttBackendProxyTransport.class, "vp:a",
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicy(MqttBackendProxyTransport.class, "vp:a",
SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED), encrypted)));
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicyAccess(RedisBackendProxyTransport.class, "vp:a",
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicy(RedisBackendProxyTransport.class, "vp:a",
SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.COMPATIBILITY), encrypted)));
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicyAccess(RedisBackendProxyTransport.class, "vp:a",
assertFalse(policy.hasEquivalentPolicy(new SharedInboundPolicy(RedisBackendProxyTransport.class, "vp:a",
SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED), TransportEnvelopeEncryption.load(keyFile,
TransportEnvelopeEncryption.Domain.PROXY_BACKEND, false))));
}
@@ -1,4 +1,4 @@
package com.bencodez.votingplugin.tests.build;
package com.bencodez.votingplugin.build;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.commands;
import com.bencodez.votingplugin.commands.AdminAuthorization;
package com.bencodez.votingplugin.commands;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -1,7 +1,4 @@
package com.bencodez.votingplugin.tests.commands;
import com.bencodez.votingplugin.commands.AdminAuthorization;
import com.bencodez.votingplugin.commands.CommandLoader;
package com.bencodez.votingplugin.commands;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
@@ -29,12 +26,12 @@ class CommandLoaderBulkPermissionTest {
PlayerCommandHandler handler = mock(PlayerCommandHandler.class);
try {
PlayerCommandHandler.class.getMethod("withAllPermissionOverrides", String[].class);
assertSame(handler, CommandLoaderAccess.configureAllPermissionOverride(handler,
assertSame(handler, CommandLoader.configureAllPermissionOverride(handler,
AdminAuthorization.ADMIN_PERMISSION));
assertTrue(mockingDetails(handler).getInvocations().stream()
.anyMatch(invocation -> invocation.getMethod().getName().equals("withAllPermissionOverrides")));
} catch (NoSuchMethodException exception) {
assertThrows(IllegalStateException.class, () -> CommandLoaderAccess.configureAllPermissionOverride(handler,
assertThrows(IllegalStateException.class, () -> CommandLoader.configureAllPermissionOverride(handler,
AdminAuthorization.ADMIN_PERMISSION));
}
}
@@ -47,7 +44,7 @@ class CommandLoaderBulkPermissionTest {
.thenReturn(List.of("VotingPlugin.Commands.AdminVote.SetPoints.All"));
when(manager.getPermission("VotingPlugin.Commands.AdminVote.SetPoints.All")).thenReturn(null);
CommandLoaderAccess.registerAdditionalPermissions(manager, handler);
CommandLoader.registerAdditionalPermissions(manager, handler);
ArgumentCaptor<Permission> registered = ArgumentCaptor.forClass(Permission.class);
verify(manager).addPermission(registered.capture());
@@ -1,8 +1,6 @@
package com.bencodez.votingplugin.tests.commands;
package com.bencodez.votingplugin.commands;
import com.bencodez.votingplugin.commands.CommandLoader;
import com.bencodez.votingplugin.tests.util.EntityTaskResultTestCompat;
import com.bencodez.votingplugin.util.EntityTaskResultTestCompat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -39,7 +37,7 @@ class CommandLoaderSchedulingTest {
Player sender = mock(Player.class);
Runnable completion = () -> { };
CommandLoaderAccess.runForCommandSender(new CommandLoader(plugin), sender, completion);
new CommandLoader(plugin).runForCommandSender(sender, completion);
verify(scheduler).runTask(eq(plugin), any(Runnable.class), eq(sender));
verify(scheduler, never()).runTask(eq(plugin), any(Runnable.class));
@@ -54,7 +52,7 @@ class CommandLoaderSchedulingTest {
CommandSender sender = mock(CommandSender.class);
Runnable completion = () -> { };
CommandLoaderAccess.runForCommandSender(new CommandLoader(plugin), sender, completion);
new CommandLoader(plugin).runForCommandSender(sender, completion);
verify(scheduler).runTask(eq(plugin), any(Runnable.class));
verify(scheduler, never()).runTask(eq(plugin), any(Runnable.class), any(Player.class));
@@ -71,7 +69,7 @@ class CommandLoaderSchedulingTest {
when(user.getPlayer()).thenReturn(recipient);
Runnable completion = () -> { };
CommandLoaderAccess.runForVotingUser(new CommandLoader(plugin), user, completion);
new CommandLoader(plugin).runForVotingUser(user, completion);
verify(scheduler).runTask(eq(plugin), any(Runnable.class), eq(recipient));
verify(scheduler, never()).runTask(eq(plugin), any(Runnable.class));
@@ -85,7 +83,7 @@ class CommandLoaderSchedulingTest {
VotingPluginUser user = mock(VotingPluginUser.class);
Runnable completion = () -> { };
CommandLoaderAccess.runForVotingUser(new CommandLoader(plugin), user, completion);
new CommandLoader(plugin).runForVotingUser(user, completion);
verify(scheduler).runTask(eq(plugin), any(Runnable.class));
verify(scheduler, never()).runTask(eq(plugin), any(Runnable.class), any(Player.class));
@@ -102,11 +100,11 @@ class CommandLoaderSchedulingTest {
CommandLoader loader = new CommandLoader(plugin);
org.junit.jupiter.api.Assertions.assertEquals("insufficient",
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));
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));
org.junit.jupiter.api.Assertions.assertEquals("pending; do not retry",
CommandLoaderAccess.transferFailureMessage(loader, PointTransferResult.PENDING_CONFIRMATION));
loader.transferFailureMessage(PointTransferResult.PENDING_CONFIRMATION));
}
@Test
@@ -137,7 +135,7 @@ class CommandLoaderSchedulingTest {
.when(storage).execute(any(Runnable.class));
AtomicBoolean storageRan = new AtomicBoolean();
CommandLoaderAccess.runBulkStorageMutation(new CommandLoader(plugin), sender, () -> storageRan.set(true), () -> { });
new CommandLoader(plugin).runBulkStorageMutation(sender, () -> storageRan.set(true), () -> { });
org.junit.jupiter.api.Assertions.assertFalse(storageRan.get());
org.junit.jupiter.api.Assertions.assertNotNull(worker.get());
@@ -165,9 +163,9 @@ class CommandLoaderSchedulingTest {
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<CommandLoaderAccess.BulkVotingUsers> delivered = new AtomicReference<>();
AtomicReference<CommandLoader.BulkVotingUsers> delivered = new AtomicReference<>();
CommandLoaderAccess.loadAllVotingUsersAsync(new CommandLoader(plugin), sender, delivered::set);
new CommandLoader(plugin).loadAllVotingUsersAsync(sender, delivered::set);
storageTasks.get(0).run();
verify(user, never()).getPlayer();
platform.get().run();
@@ -198,7 +196,7 @@ class CommandLoaderSchedulingTest {
org.mockito.Mockito.doAnswer(call -> { platformTasks.add(call.getArgument(1)); return null; })
.when(scheduler).runTask(eq(plugin), any(Runnable.class));
CommandLoaderAccess.loadAllVotingUsersAsync(new CommandLoader(plugin), sender, ignored -> { });
new CommandLoader(plugin).loadAllVotingUsersAsync(sender, ignored -> { });
storageTasks.get(0).run();
org.junit.jupiter.api.Assertions.assertEquals(1, platformTasks.size());
platformTasks.get(0).run();
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.commands;
import com.bencodez.votingplugin.commands.CommandLoader;
package com.bencodez.votingplugin.commands;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -135,25 +133,20 @@ class OfflineVoteClearCommandRegistrationTest {
}
}
private static final class TestLoader {
private final CommandLoader delegate;
private static final class TestLoader extends CommandLoader {
private final AtomicReference<java.util.function.BooleanSupplier> 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());
}
TestLoader(VotingPluginMain plugin, AtomicReference<java.util.function.BooleanSupplier> mutation) {
super(plugin);
this.mutation = mutation;
}
void registerOfflineVoteClearCommands(String permission) {
CommandLoaderAccess.registerOfflineVoteClearCommands(delegate, permission);
}
}
@Override
void runForCommandSender(CommandSender sender, Runnable task) { task.run(); }
@Override
void runConfirmedBulkStorageMutation(CommandSender sender, java.util.function.BooleanSupplier work, Runnable success) {
mutation.set(work);
}
}
}
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.commands.gui.admin;
import com.bencodez.votingplugin.commands.gui.admin.AdminVotePerms;
package com.bencodez.votingplugin.commands.gui.admin;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -39,14 +37,14 @@ class AdminVotePermsTest {
"VotingPlugin.Commands.AdminVote.AddPoints.All");
assertEquals(Collections.singletonList("VotingPlugin.Commands.AdminVote.SetPoints.All"),
AdminVotePermsAccess.additionalPermissions(setPoints));
AdminVotePerms.additionalPermissions(setPoints));
assertEquals(Collections.singletonList("VotingPlugin.Commands.AdminVote.AddPoints.All"),
AdminVotePermsAccess.additionalPermissions(addPoints));
AdminVotePerms.additionalPermissions(addPoints));
}
@Test
void ordinaryCommandsDoNotInventAdditionalPermissions() {
assertEquals(Collections.emptyList(), AdminVotePermsAccess.additionalPermissions(mock(CommandHandler.class)));
assertEquals(Collections.emptyList(), AdminVotePerms.additionalPermissions(mock(CommandHandler.class)));
}
@Test
@@ -127,7 +125,7 @@ class AdminVotePermsTest {
void editorPermissionStatusIncludesAdminOverride() {
CommandSender sender = mock(CommandSender.class);
when(sender.hasPermission(AdminAuthorization.ADMIN_PERMISSION)).thenReturn(true);
assertTrue(AdminVotePermsAccess.hasEffectivePermission(sender, "VotingPlugin.Commands.AdminVote.Edit.Config"));
assertTrue(AdminVotePerms.hasEffectivePermission(sender, "VotingPlugin.Commands.AdminVote.Edit.Config"));
}
@Test
@@ -217,7 +215,6 @@ 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);
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.commands.gui.player;
import com.bencodez.votingplugin.commands.gui.player.VoteGUI;
package com.bencodez.votingplugin.commands.gui.player;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -38,7 +36,7 @@ class VoteGUIPermissionTest {
assertNotEquals(player.getUniqueId().toString(), viewedUser.getUUID());
assertEquals("VotingPlugin.Commands.Vote.GUI",
VoteGUIAccess.getRequiredPermission(plugin, player, viewedUser));
VoteGUI.getRequiredPermission(plugin, player, viewedUser));
}
@Test
@@ -55,7 +53,7 @@ class VoteGUIPermissionTest {
when(viewedUser.getUUID()).thenReturn(null);
assertEquals("VotingPlugin.Commands.Vote.GUI.Other",
VoteGUIAccess.getRequiredPermission(plugin, player, viewedUser));
VoteGUI.getRequiredPermission(plugin, player, viewedUser));
}
@Test
@@ -72,10 +70,10 @@ class VoteGUIPermissionTest {
when(viewedUser.getUUID()).thenReturn("33333333-3333-3333-3333-333333333333");
assertEquals("VotingPlugin.Commands.Vote.GUI.Other",
VoteGUIAccess.getRequiredPermission(plugin, player, viewedUser));
VoteGUI.getRequiredPermission(plugin, player, viewedUser));
when(player.hasPermission("VotingPlugin.Commands.Vote.GUI.Other")).thenReturn(false, true);
assertFalse(VoteGUIAccess.isAuthorized(plugin, player, viewedUser));
assertTrue(VoteGUIAccess.isAuthorized(plugin, player, viewedUser));
assertFalse(VoteGUI.isAuthorized(plugin, player, viewedUser));
assertTrue(VoteGUI.isAuthorized(plugin, player, viewedUser));
}
@Test
@@ -91,8 +89,8 @@ class VoteGUIPermissionTest {
when(viewedUser.getUUID()).thenReturn("target");
when(player.hasPermission("VotingPlugin.Commands.Vote.GUI.Other")).thenReturn(true, false);
assertTrue(VoteGUIAccess.isAuthorized(plugin, player, viewedUser), "dialog may initially open");
assertFalse(VoteGUIAccess.isAuthorized(plugin, player, viewedUser),
assertTrue(VoteGUI.isAuthorized(plugin, player, viewedUser), "dialog may initially open");
assertFalse(VoteGUI.isAuthorized(plugin, player, viewedUser),
"dialog action must observe a permission revoked after opening");
}
}
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.commands.gui.player;
import com.bencodez.votingplugin.tests.commands.gui.player.VoteShopCategoryAuthorizationAccess;
package com.bencodez.votingplugin.commands.gui.player;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -22,17 +20,17 @@ class VoteShopCategoryAuthorizationTest {
VoteShopCategoryButton button = button("restricted", "shop.category", false);
VoteShopDefinition definition = definition(button);
assertTrue(VoteShopCategoryAuthorizationAccess.mayDisplay(button, permission -> false));
assertFalse(VoteShopCategoryAuthorizationAccess.authorized(button, permission -> false));
assertFalse(VoteShopCategoryAuthorizationAccess.canOpen(definition, "restricted", permission -> false));
assertTrue(VoteShopCategoryAuthorization.mayDisplay(button, permission -> false));
assertFalse(VoteShopCategoryAuthorization.authorized(button, permission -> false));
assertFalse(VoteShopCategoryAuthorization.canOpen(definition, "restricted", permission -> false));
}
@Test
void hideFlagControlsVisibilityWithoutChangingAuthorization() {
VoteShopCategoryButton hidden = button("restricted", "shop.category", true);
assertFalse(VoteShopCategoryAuthorizationAccess.mayDisplay(hidden, permission -> false));
assertTrue(VoteShopCategoryAuthorizationAccess.mayDisplay(hidden, permission -> true));
assertFalse(VoteShopCategoryAuthorization.mayDisplay(hidden, permission -> false));
assertTrue(VoteShopCategoryAuthorization.mayDisplay(hidden, permission -> true));
}
@Test
@@ -48,11 +46,11 @@ class VoteShopCategoryAuthorizationTest {
child.setId("child");
definition.getCategories().put("child", child);
assertFalse(VoteShopCategoryAuthorizationAccess.canOpen(definition, "child",
assertFalse(VoteShopCategoryAuthorization.canOpen(definition, "child",
Set.of("shop.child")::contains));
assertFalse(VoteShopCategoryAuthorizationAccess.canOpen(definition, "child",
assertFalse(VoteShopCategoryAuthorization.canOpen(definition, "child",
Set.of("shop.parent")::contains));
assertTrue(VoteShopCategoryAuthorizationAccess.canOpen(definition, "child",
assertTrue(VoteShopCategoryAuthorization.canOpen(definition, "child",
Set.of("shop.parent", "shop.child")::contains));
}
@@ -65,14 +63,14 @@ class VoteShopCategoryAuthorizationTest {
category.getEntries().put("item", item);
AtomicBoolean permitted = new AtomicBoolean(true);
assertTrue(VoteShopCategoryAuthorizationAccess.canPurchase(definition, category, item,
assertTrue(VoteShopCategoryAuthorization.canPurchase(definition, category, item,
permission -> permitted.get()));
permitted.set(false);
assertFalse(VoteShopCategoryAuthorizationAccess.canPurchase(definition, category, item,
assertFalse(VoteShopCategoryAuthorization.canPurchase(definition, category, item,
permission -> permitted.get()));
VoteShopItem mainItem = item("main");
definition.getMainEntries().put("main", mainItem);
assertTrue(VoteShopCategoryAuthorizationAccess.canPurchase(definition, null, mainItem, permission -> false));
assertTrue(VoteShopCategoryAuthorization.canPurchase(definition, null, mainItem, permission -> false));
}
@Test
@@ -84,7 +82,7 @@ class VoteShopCategoryAuthorizationTest {
definition.getCategories().remove("restricted");
assertFalse(VoteShopCategoryAuthorizationAccess.canPurchase(definition, staleCategory, item,
assertFalse(VoteShopCategoryAuthorization.canPurchase(definition, staleCategory, item,
permission -> true));
}
@@ -99,7 +97,7 @@ class VoteShopCategoryAuthorizationTest {
replacement.getEntries().put("item", item("item"));
definition.getCategories().put("restricted", replacement);
assertFalse(VoteShopCategoryAuthorizationAccess.canPurchase(definition, staleCategory, staleItem,
assertFalse(VoteShopCategoryAuthorization.canPurchase(definition, staleCategory, staleItem,
permission -> true));
}
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.commands.gui.player;
import com.bencodez.votingplugin.commands.gui.player.VoteShopConfirm;
package com.bencodez.votingplugin.commands.gui.player;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -12,7 +10,7 @@ class VoteShopConfirmTest {
void confirmationCanSubmitOnlyOnePurchase() {
VoteShopConfirm confirmation = new VoteShopConfirm(null, null, null, null, null);
assertTrue(VoteShopConfirmAccess.beginPurchase(confirmation));
assertFalse(VoteShopConfirmAccess.beginPurchase(confirmation));
assertTrue(confirmation.beginPurchase());
assertFalse(confirmation.beginPurchase());
}
}
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.control;
import com.bencodez.votingplugin.control.BackendConfigurationService;
package com.bencodez.votingplugin.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@@ -39,7 +37,7 @@ class BackendConfigurationServiceTest {
@Test void transientReadFailuresAreRetried() throws Exception {
AtomicInteger attempts = new AtomicInteger();
String value = BackendConfigurationServiceAccess.retryRead(() -> {
String value = BackendConfigurationService.retryRead(() -> {
if (attempts.incrementAndGet() < 3) throw new IOException("configuration is being replaced");
return "loaded";
});
@@ -49,17 +47,17 @@ class BackendConfigurationServiceTest {
}
@Test void nullYamlCommentMetadataDoesNotBreakControlReads() {
List<String> sanitized = BackendConfigurationServiceAccess.sanitizeComments(
List<String> sanitized = BackendConfigurationService.sanitizeComments(
Arrays.asList(null, "# Token: generated-secret", "# ordinary owner note"),
List.of("generated-secret"), false);
assertEquals(Arrays.asList(null, "# Token: " + BackendConfigurationService.REDACTED,
"# ordinary owner note"), sanitized);
assertEquals(List.of(), BackendConfigurationServiceAccess.sanitizeComments(null, List.of(), false));
assertEquals(List.of(), BackendConfigurationService.sanitizeComments(null, List.of(), false));
}
@Test void malformedQuickReadInputRemainsAValidationFailure() {
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertThrows(IllegalArgumentException.class, () -> service.readQuickSetup("vote-site", Map.of()));
assertThrows(IllegalArgumentException.class, () -> service.readQuickSetup("vote-site",
@@ -77,7 +75,7 @@ class BackendConfigurationServiceTest {
Path config = directory.resolve("Config.yml");
Files.writeString(config, "Database:\n Password: keep-me\nFeature: false\n");
AtomicInteger reloads = new AtomicInteger();
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, reloads::incrementAndGet);
BackendConfigurationService service = new BackendConfigurationService(directory, reloads::incrementAndGet);
BackendConfigurationService.Document read = service.read("Config.yml");
assertFalse(read.content().contains("keep-me"));
@@ -104,7 +102,7 @@ class BackendConfigurationServiceTest {
+ " Password: keep-me # database credential\n"
+ "Feature: false # toggle from Control\n"
+ "# End of owner notes\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.Document read = service.read("Config.yml");
assertFalse(read.content().contains("keep-me"));
@@ -141,7 +139,7 @@ class BackendConfigurationServiceTest {
+ "Other: true # Authorization: Bearer old-token\n"
+ "Hook: true # WebhookURL: https://example.invalid/private hook\n"
+ "Quoted: true # DatabasePassword: \"two words\"\n");
BackendConfigurationService.Document read = BackendConfigurationServiceAccess.create(directory, () -> { })
BackendConfigurationService.Document read = new BackendConfigurationService(directory, () -> { })
.read("Config.yml");
assertFalse(read.content().contains("commented-secret"));
@@ -158,7 +156,7 @@ class BackendConfigurationServiceTest {
Path config = directory.resolve("Config.yml");
Files.writeString(config, "# User: database-admin\n# DatabaseUsername=database-owner\nFeature: false\n");
BackendConfigurationService.Document read = BackendConfigurationServiceAccess.create(directory, () -> { })
BackendConfigurationService.Document read = new BackendConfigurationService(directory, () -> { })
.read("Config.yml");
assertFalse(read.content().contains("database-admin"));
@@ -169,7 +167,7 @@ class BackendConfigurationServiceTest {
@Test void redactsSecretsInCommentOnlyDocuments() throws Exception {
Files.writeString(directory.resolve("Config.yml"), "# Password: header-secret\n# owner footer\n");
BackendConfigurationService.Document read = BackendConfigurationServiceAccess.create(directory, () -> { })
BackendConfigurationService.Document read = new BackendConfigurationService(directory, () -> { })
.read("Config.yml");
assertFalse(read.content().contains("header-secret"));
assertTrue(read.content().contains(BackendConfigurationService.REDACTED));
@@ -178,7 +176,7 @@ class BackendConfigurationServiceTest {
@Test void doesNotRewriteHashTextInsideBlockScalars() throws Exception {
Path config = directory.resolve("Config.yml");
Files.writeString(config, "Message: |\n # Password: this is message text\nFeature: false\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.Document read = service.read("Config.yml");
assertTrue(read.content().contains("# Password: this is message text"));
@@ -191,7 +189,7 @@ class BackendConfigurationServiceTest {
Files.writeString(config, "Password: true\n# DiscordWebhook.URL: https://dotted-secret.invalid/hook\n"
+ "DiscordWebhook:\n URL: '' # \"URL\": https://old-secret.invalid/hook\n"
+ "Feature: false # This is true when enabled\n");
BackendConfigurationService.Document read = BackendConfigurationServiceAccess.create(directory, () -> { })
BackendConfigurationService.Document read = new BackendConfigurationService(directory, () -> { })
.read("Config.yml");
assertFalse(read.content().contains("old-secret.invalid"));
@@ -204,7 +202,7 @@ class BackendConfigurationServiceTest {
Files.writeString(config, "Password: |\n alpha-secret-part\n beta-secret-part\n"
+ "# rotate alpha-secret-part soon\nFeature: false\n");
BackendConfigurationService.Document read = BackendConfigurationServiceAccess.create(directory, () -> { })
BackendConfigurationService.Document read = new BackendConfigurationService(directory, () -> { })
.read("Config.yml");
assertFalse(read.content().contains("alpha-secret-part"));
@@ -215,7 +213,7 @@ class BackendConfigurationServiceTest {
Path config = directory.resolve("Config.yml");
Files.writeString(config, "# Password:\n# real-comment-secret\nFeature: false\n");
BackendConfigurationService.Document read = BackendConfigurationServiceAccess.create(directory, () -> { })
BackendConfigurationService.Document read = new BackendConfigurationService(directory, () -> { })
.read("Config.yml");
assertFalse(read.content().contains("real-comment-secret"));
@@ -225,7 +223,7 @@ class BackendConfigurationServiceTest {
Path config = directory.resolve("Config.yml");
Files.writeString(config, "# Password: |\n# first-comment-secret\n# second-comment-secret\n#\n"
+ "# owner note\nFeature: false\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.Document read = service.read("Config.yml");
assertFalse(read.content().contains("first-comment-secret"));
@@ -243,7 +241,7 @@ class BackendConfigurationServiceTest {
Files.writeString(config, "# Password: |\n# literal-comment-secret\n"
+ "# Token: >-\n# folded-comment-secret\n"
+ "# ApiKey: |2+\n# indented-comment-secret\nFeature: false\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.Document read = service.read("Config.yml");
assertFalse(read.content().contains("literal-comment-secret"));
@@ -267,7 +265,7 @@ class BackendConfigurationServiceTest {
@Test void rejectsChangedRedactedCommentPlaceholders() throws Exception {
Files.writeString(directory.resolve("Config.yml"), "# Password: comment-secret\nFeature: false\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.Document read = service.read("Config.yml");
String changed = read.content().replace("Password: " + BackendConfigurationService.REDACTED,
"Password changed: " + BackendConfigurationService.REDACTED);
@@ -277,7 +275,7 @@ class BackendConfigurationServiceTest {
@Test void rejectsRemovedOrReplacedRedactedCommentPlaceholders() throws Exception {
Files.writeString(directory.resolve("Config.yml"), "# Password: comment-secret\nFeature: false\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.Document read = service.read("Config.yml");
String removed = read.content().lines().filter(line -> !line.contains(BackendConfigurationService.REDACTED))
.collect(java.util.stream.Collectors.joining("\n")) + "\n";
@@ -291,7 +289,7 @@ class BackendConfigurationServiceTest {
@Test void rejectsRemovingAKeyThatOwnsARedactedComment() throws Exception {
Files.writeString(directory.resolve("Config.yml"),
"Feature: false # Password: inline-comment-secret\nOther: true\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.Document read = service.read("Config.yml");
assertTrue(read.content().contains(BackendConfigurationService.REDACTED));
@@ -301,7 +299,7 @@ class BackendConfigurationServiceTest {
@Test void comparesRedactedCommentOwnersRegardlessOfMappingOrder() throws Exception {
Files.writeString(directory.resolve("Config.yml"), "Alpha:\n Enabled: true # Password: alpha-secret\n"
+ "Beta:\n Enabled: true # Token: beta-secret\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.Document read = service.read("Config.yml");
String proposal = "Beta:\n Enabled: false # Token: " + BackendConfigurationService.REDACTED + "\n"
@@ -323,14 +321,14 @@ class BackendConfigurationServiceTest {
< BackendConfigurationService.MAX_CONTENT_BYTES);
Files.writeString(directory.resolve("Config.yml"), raw);
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertThrows(IOException.class, () -> service.read("Config.yml"));
}
@Test void reportsCommentOnlyChangesInPreview() throws Exception {
Files.writeString(directory.resolve("Config.yml"), "# original owner note\nFeature: false # old inline note\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.Document read = service.read("Config.yml");
BackendConfigurationService.Preview preview = service.preview("Config.yml", read.content()
@@ -344,7 +342,7 @@ class BackendConfigurationServiceTest {
Path config = directory.resolve("Config.yml");
Files.writeString(config, "# \"Password\": quoted-comment-secret\nFeature: false\n");
BackendConfigurationService.Document read = BackendConfigurationServiceAccess.create(directory, () -> { })
BackendConfigurationService.Document read = new BackendConfigurationService(directory, () -> { })
.read("Config.yml");
assertFalse(read.content().contains("quoted-comment-secret"));
@@ -352,7 +350,7 @@ class BackendConfigurationServiceTest {
@Test void rejectsStaleInvalidAndUnmanagedWrites() throws Exception {
Files.writeString(directory.resolve("Config.yml"), "Feature: false\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
String revision = service.read("Config.yml").revision();
Files.writeString(directory.resolve("Config.yml"), "Feature: manual\n");
assertThrows(BackendConfigurationService.StaleRevisionException.class,
@@ -367,7 +365,7 @@ class BackendConfigurationServiceTest {
Path externalConfig = outside.resolve("Config.yml");
Files.writeString(externalConfig, "Feature: external\n");
Files.createSymbolicLink(root.resolve("Config.yml"), externalConfig);
BackendConfigurationService service = BackendConfigurationServiceAccess.create(root, () -> { });
BackendConfigurationService service = new BackendConfigurationService(root, () -> { });
assertThrows(java.io.IOException.class, () -> service.read("Config.yml"));
Files.createDirectory(outside.resolve("sites"));
@@ -382,9 +380,9 @@ class BackendConfigurationServiceTest {
Path rewards = Files.createDirectories(directory.resolve("Rewards"));
Files.writeString(rewards.resolve("Daily.yml"), "Commands: []\n");
Files.writeString(rewards.resolve("Standard.yml"), "Messages: {}\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertEquals(List.of("Daily.yml", "Standard.yml"), BackendConfigurationServiceAccess.rewardFileInventory(directory));
assertEquals(List.of("Daily.yml", "Standard.yml"), BackendConfigurationService.rewardFileInventory(directory));
assertDoesNotThrow(() -> service.read("Rewards/Daily.yml"));
assertThrows(IllegalArgumentException.class, () -> service.read("Rewards/../Config.yml"));
assertThrows(IllegalArgumentException.class, () -> service.read("Rewards/%2e%2e%2fConfig.yml"));
@@ -396,38 +394,38 @@ class BackendConfigurationServiceTest {
}
@Test void onlyAnAbsentRewardsDirectoryProducesAnEmptyInventory() throws Exception {
assertEquals(List.of(), BackendConfigurationServiceAccess.rewardFileInventory(directory));
assertThrows(IOException.class, () -> BackendConfigurationServiceAccess.rewardFileInventory(directory.resolve("missing")));
assertEquals(List.of(), BackendConfigurationService.rewardFileInventory(directory));
assertThrows(IOException.class, () -> BackendConfigurationService.rewardFileInventory(directory.resolve("missing")));
Files.createDirectory(directory.resolve("Rewards"));
assertThrows(java.nio.file.NoSuchFileException.class,
() -> BackendConfigurationServiceAccess.rewardFileInventory(directory,
() -> BackendConfigurationService.rewardFileInventory(directory,
ignored -> { throw new java.nio.file.NoSuchFileException("disappeared-entry.yml"); }));
}
@Test void namedRewardCapabilityRequiresSecureDirectoryAndPrivateFileSupport() throws Exception {
assertTrue(BackendConfigurationServiceAccess.supportsNamedRewardFiles(directory));
assertTrue(BackendConfigurationService.supportsNamedRewardFiles(directory));
try (var entries = Files.list(directory)) {
assertFalse(entries.anyMatch(path -> path.getFileName().toString().startsWith(".control-capability-")));
}
assertFalse(BackendConfigurationServiceAccess.supportsNamedRewardFiles(directory,
assertFalse(BackendConfigurationService.supportsNamedRewardFiles(directory,
ignored -> { throw new IOException("directory forcing is unavailable"); }));
Path outside = Files.createDirectory(directory.resolve("outside"));
Files.createSymbolicLink(directory.resolve("Rewards"), outside);
assertFalse(BackendConfigurationServiceAccess.supportsNamedRewardFiles(directory));
assertFalse(BackendConfigurationService.supportsNamedRewardFiles(directory));
Files.delete(directory.resolve("Rewards"));
Files.createDirectory(directory.resolve("Rewards"));
assertTrue(BackendConfigurationServiceAccess.supportsNamedRewardFiles(directory));
assertTrue(BackendConfigurationService.supportsNamedRewardFiles(directory));
AtomicInteger checkedRewardDirectory = new AtomicInteger();
assertFalse(BackendConfigurationServiceAccess.supportsNamedRewardFiles(directory,
assertFalse(BackendConfigurationService.supportsNamedRewardFiles(directory,
channel -> channel.force(true), ignored -> {
checkedRewardDirectory.incrementAndGet();
return false;
}));
assertEquals(1, checkedRewardDirectory.get());
assertFalse(BackendConfigurationServiceAccess.supportsNamedRewardFiles(directory,
assertFalse(BackendConfigurationService.supportsNamedRewardFiles(directory,
channel -> channel.force(true),
ignored -> { throw new java.nio.file.NoSuchFileException("vanished-reward-entry"); }));
assertFalse(BackendConfigurationServiceAccess.supportsNamedRewardFiles(directory,
assertFalse(BackendConfigurationService.supportsNamedRewardFiles(directory,
channel -> channel.force(true), pinned -> {
assertTrue(pinned.getFileAttributeView(PosixFileAttributeView.class) != null);
throw new UnsupportedOperationException("private staging creation is unavailable");
@@ -438,21 +436,21 @@ class BackendConfigurationServiceTest {
Path archive = directory.resolve("unsupported.zip");
try (var zip = FileSystems.newFileSystem(URI.create("jar:" + archive.toUri()), Map.of("create", "true"))) {
Path plugin = Files.createDirectory(zip.getPath("/plugin"));
assertFalse(BackendConfigurationServiceAccess.supportsNamedRewardFiles(plugin));
assertFalse(BackendConfigurationService.supportsNamedRewardFiles(plugin));
}
}
@Test void namedRewardCapabilityReprobesAfterTransientStagingFailure() throws Exception {
Files.createDirectory(directory.resolve("Rewards"));
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
AtomicInteger attempts = new AtomicInteger();
BackendConfigurationServiceAccess.DirectoryStagingSupport probe = ignored -> {
BackendConfigurationService.DirectoryStagingSupport probe = ignored -> {
if (attempts.incrementAndGet() == 1) throw new IOException("temporary staging failure");
return true;
};
assertFalse(BackendConfigurationServiceAccess.supportsNamedRewardFiles(service, probe));
assertTrue(BackendConfigurationServiceAccess.supportsNamedRewardFiles(service, probe));
assertTrue(BackendConfigurationServiceAccess.supportsNamedRewardFiles(service, probe));
assertFalse(service.supportsNamedRewardFiles(probe));
assertTrue(service.supportsNamedRewardFiles(probe));
assertTrue(service.supportsNamedRewardFiles(probe));
assertEquals(2, attempts.get(), "only a successful probe may be cached for this directory");
}
@@ -461,7 +459,7 @@ class BackendConfigurationServiceTest {
Path rewards = Files.createDirectories(directory.resolve("Rewards"));
Path file = rewards.resolve("Daily.yml");
Files.writeString(file, "Money: 1\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
String revision = service.read("Rewards/Daily.yml").revision();
service.apply("Rewards/Daily.yml", "Money: 2\n", revision);
@@ -490,7 +488,7 @@ class BackendConfigurationServiceTest {
});
try {
worker.submit(() -> assertThrows(IOException.class,
() -> BackendConfigurationServiceAccess.readRaw(pinned, "Daily.yml", false)))
() -> BackendConfigurationService.readRaw(pinned, "Daily.yml", false)))
.get(2, TimeUnit.SECONDS);
} finally {
worker.shutdownNow();
@@ -516,7 +514,7 @@ class BackendConfigurationServiceTest {
assertEquals("Money: 1\n", expectedContent);
}
};
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, reload);
BackendConfigurationService service = new BackendConfigurationService(directory, reload);
String revision = service.read("Rewards/Daily.yml").revision();
BackendConfigurationService.ApplyFailureException failure = assertThrows(
BackendConfigurationService.ApplyFailureException.class,
@@ -531,13 +529,13 @@ class BackendConfigurationServiceTest {
Path outside = directory.resolve("outside.yml");
Files.writeString(outside, "outside: true\n");
Files.createSymbolicLink(rewards.resolve("External.yml"), outside);
assertThrows(IOException.class, () -> BackendConfigurationServiceAccess.rewardFileInventory(directory));
assertThrows(IOException.class, () -> BackendConfigurationService.rewardFileInventory(directory));
Files.delete(rewards.resolve("External.yml"));
Files.writeString(rewards.resolve("Daily.yml"), "one: true\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
String revision = service.read("Rewards/Daily.yml").revision();
Files.writeString(rewards.resolve("daily.yml"), "two: true\n");
assertThrows(IOException.class, () -> BackendConfigurationServiceAccess.rewardFileInventory(directory));
assertThrows(IOException.class, () -> BackendConfigurationService.rewardFileInventory(directory));
assertThrows(IOException.class, () -> service.read("Rewards/Daily.yml"));
assertThrows(IOException.class, () -> service.preview("Rewards/Daily.yml", "one: false\n"));
assertThrows(IOException.class, () -> service.apply("Rewards/Daily.yml", "one: false\n", revision));
@@ -546,22 +544,22 @@ class BackendConfigurationServiceTest {
for (int index = 0; index <= BackendConfigurationService.MAX_REWARD_FILES; index++) {
Files.writeString(rewards.resolve("Reward" + index + ".yml"), "value: true\n");
}
assertThrows(IOException.class, () -> BackendConfigurationServiceAccess.rewardFileInventory(directory));
assertThrows(IOException.class, () -> BackendConfigurationService.rewardFileInventory(directory));
}
@Test void rewardInventoryBoundsIgnoredEntriesBeforeReadPreviewOrApply() throws Exception {
Path rewards = Files.createDirectories(directory.resolve("Rewards"));
Path daily = rewards.resolve("Daily.yml");
Files.writeString(daily, "Money: 1\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
String revision = service.read("Rewards/Daily.yml").revision();
for (int index = 0; index < BackendConfigurationServiceAccess.MAX_REWARD_DIRECTORY_ENTRIES - 1; index++) {
for (int index = 0; index < BackendConfigurationService.MAX_REWARD_DIRECTORY_ENTRIES - 1; index++) {
Files.createDirectory(rewards.resolve("Ignored" + index));
}
assertEquals(List.of("Daily.yml"), BackendConfigurationServiceAccess.rewardFileInventory(directory));
assertEquals(List.of("Daily.yml"), BackendConfigurationService.rewardFileInventory(directory));
assertEquals(revision, service.read("Rewards/Daily.yml").revision());
Files.createDirectory(rewards.resolve("Overflow"));
assertThrows(IOException.class, () -> BackendConfigurationServiceAccess.rewardFileInventory(directory));
assertThrows(IOException.class, () -> BackendConfigurationService.rewardFileInventory(directory));
assertThrows(IOException.class, () -> service.read("Rewards/Daily.yml"));
assertThrows(IOException.class, () -> service.preview("Rewards/Daily.yml", "Money: 2\n"));
assertThrows(IOException.class, () -> service.apply("Rewards/Daily.yml", "Money: 2\n", revision));
@@ -575,7 +573,7 @@ class BackendConfigurationServiceTest {
Files.writeString(rewards.resolve("Daily.yml"), "Money: 1\n");
Files.writeString(outside.resolve("Daily.yml"), "Money: 999\n");
AtomicInteger reloads = new AtomicInteger();
BackendConfigurationService service = BackendConfigurationServiceAccess.create(plugin,
BackendConfigurationService service = new BackendConfigurationService(plugin,
(BackendConfigurationService.ApplyAction) ignored -> {
if (reloads.incrementAndGet() == 1) {
Files.move(rewards, plugin.resolve("Rewards-original"));
@@ -600,7 +598,7 @@ class BackendConfigurationServiceTest {
Files.writeString(rewards.resolve("Daily.yml"), "Money: 1\n");
Files.writeString(replacement.resolve("Daily.yml"), "Money: 999\n");
AtomicInteger reloads = new AtomicInteger();
BackendConfigurationService service = BackendConfigurationServiceAccess.create(plugin,
BackendConfigurationService service = new BackendConfigurationService(plugin,
(BackendConfigurationService.ApplyAction) ignored -> {
if (reloads.incrementAndGet() == 1) {
Files.move(rewards, plugin.resolve("Rewards-original"));
@@ -644,7 +642,7 @@ class BackendConfigurationServiceTest {
assertEquals("Money: 1\n", expectedContent);
}
};
BackendConfigurationService service = BackendConfigurationServiceAccess.create(plugin, reload);
BackendConfigurationService service = new BackendConfigurationService(plugin, reload);
String revision = service.read("Rewards/Daily.yml").revision();
BackendConfigurationService.ApplyFailureException failure = assertThrows(
BackendConfigurationService.ApplyFailureException.class,
@@ -675,7 +673,7 @@ class BackendConfigurationServiceTest {
reloads.incrementAndGet();
}
};
BackendConfigurationService service = BackendConfigurationServiceAccess.create(plugin, action);
BackendConfigurationService service = new BackendConfigurationService(plugin, action);
String revision = service.read("Rewards/Daily.yml").revision();
BackendConfigurationService.ApplyFailureException failure = assertThrows(
BackendConfigurationService.ApplyFailureException.class,
@@ -692,7 +690,7 @@ class BackendConfigurationServiceTest {
Files.writeString(file, "Money: 1\n");
CountDownLatch firstReload = new CountDownLatch(1);
CountDownLatch allowReload = new CountDownLatch(1);
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory,
BackendConfigurationService service = new BackendConfigurationService(directory,
(BackendConfigurationService.ApplyAction) ignored -> {
firstReload.countDown();
if (!allowReload.await(5, TimeUnit.SECONDS)) throw new IOException("test reload timed out");
@@ -720,7 +718,7 @@ class BackendConfigurationServiceTest {
Files.writeString(config, "Feature: false\n");
Files.writeString(external, "external: preserved\n");
Files.createSymbolicLink(directory.resolve("Config.yml.control-backup"), external);
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
String revision = service.read("Config.yml").revision();
assertThrows(java.io.IOException.class,
@@ -733,7 +731,7 @@ class BackendConfigurationServiceTest {
Path settings = directory.resolve("BungeeSettings.yml");
Files.writeString(settings, "UseBungeecord: false\nBungeeMethod: PLUGINMESSAGING\n");
AtomicInteger reloads = new AtomicInteger();
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory,
BackendConfigurationService service = new BackendConfigurationService(directory,
(BackendConfigurationService.ApplyAction) fileName -> {
assertEquals("BungeeSettings.yml", fileName);
if (reloads.incrementAndGet() == 1) throw new IllegalStateException("transport unavailable");
@@ -754,7 +752,7 @@ class BackendConfigurationServiceTest {
Files.writeString(config, "Feature: before\n");
AtomicInteger reloads = new AtomicInteger();
AtomicInteger moves = new AtomicInteger();
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory,
BackendConfigurationService service = new BackendConfigurationService(directory,
(BackendConfigurationService.ApplyAction) fileName -> {
if (reloads.incrementAndGet() == 1) throw new IOException("reload failed");
}, (source, target) -> {
@@ -780,7 +778,7 @@ class BackendConfigurationServiceTest {
Path config = directory.resolve("Config.yml");
Files.writeString(config, "Feature: before\n");
AtomicInteger reloads = new AtomicInteger();
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory,
BackendConfigurationService service = new BackendConfigurationService(directory,
(BackendConfigurationService.ApplyAction) fileName -> {
reloads.incrementAndGet();
Files.writeString(config, "Feature: manual\n");
@@ -801,7 +799,7 @@ class BackendConfigurationServiceTest {
Path config = directory.resolve("Config.yml");
Files.writeString(config, "Feature: before\n");
AtomicInteger reloads = new AtomicInteger();
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory,
BackendConfigurationService service = new BackendConfigurationService(directory,
(BackendConfigurationService.ApplyAction) fileName -> {
if (reloads.incrementAndGet() == 1) Files.writeString(config, "Feature: manual\n");
});
@@ -817,7 +815,7 @@ class BackendConfigurationServiceTest {
Path config = directory.resolve("Config.yml");
Files.writeString(config, "Feature: before\n");
AtomicInteger reloads = new AtomicInteger();
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory,
BackendConfigurationService service = new BackendConfigurationService(directory,
(BackendConfigurationService.ApplyAction) fileName -> {
int reload = reloads.incrementAndGet();
if (reload <= 2) Files.writeString(config, "Feature: manual-" + reload + "\n");
@@ -833,7 +831,7 @@ class BackendConfigurationServiceTest {
@Test void quickSetupsProduceReviewableBackendAndVoteSiteChanges() throws Exception {
Files.writeString(directory.resolve("BungeeSettings.yml"), "UseBungeecord: false\nServer: PleaseSet\n");
Files.writeString(directory.resolve("VoteSites.yml"), "VoteSites: {}\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickPreview proxy = service.previewQuickSetup("proxy-backend",
Map.of("server", "lobby", "method", "PLUGINMESSAGING"));
@@ -885,7 +883,7 @@ class BackendConfigurationServiceTest {
Files.writeString(directory.resolve("SpecialRewards.yml"), "VoteParty:\n Enabled: true\n"
+ " VotesRequired: 35\n GiveAllPlayers: true\n GiveOnlinePlayersOnly: false\n"
+ " Broadcast: Party!\n Rewards:\n Commands: [say one, say two]\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertEquals("lobby", service.readQuickSetup("proxy-backend", Map.of()).options().get("server"));
assertEquals("REDIS", service.readQuickSetup("proxy-backend", Map.of()).options().get("method"));
@@ -903,7 +901,7 @@ class BackendConfigurationServiceTest {
Path rewards = directory.resolve("SpecialRewards.yml");
Files.writeString(rewards, "VoteParty:\n Enabled: false\n VotesRequired: 20\n"
+ " GiveAllPlayers: false\n GiveOnlinePlayersOnly: true\n Broadcast: Keep\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickState disabled = service.readQuickSetup("vote-party", Map.of());
assertEquals("false", disabled.options().get("enabled"));
@@ -911,21 +909,21 @@ class BackendConfigurationServiceTest {
"enabled", disabled.options().get("enabled"), "votesRequired", "25", "command", "",
"broadcast", "Keep", "giveAllPlayers", "false", "onlineOnly", "true"));
assertTrue(unrelated.proposal().content().contains("Enabled: false"));
BackendConfigurationServiceAccess.applyQuickSetup(service, "vote-party", Map.of("enabled", "false", "votesRequired", "25", "command", "",
service.applyQuickSetup("vote-party", Map.of("enabled", "false", "votesRequired", "25", "command", "",
"broadcast", "Keep", "giveAllPlayers", "false", "onlineOnly", "true"), unrelated.revision());
assertEquals("false", service.readQuickSetup("vote-party", Map.of()).options().get("enabled"));
BackendConfigurationService.QuickPreview enable = service.previewQuickSetup("vote-party", Map.of(
"enabled", "true", "votesRequired", "25", "command", "", "broadcast", "Keep",
"giveAllPlayers", "false", "onlineOnly", "true"));
BackendConfigurationServiceAccess.applyQuickSetup(service, "vote-party", Map.of("enabled", "true", "votesRequired", "25", "command", "",
service.applyQuickSetup("vote-party", Map.of("enabled", "true", "votesRequired", "25", "command", "",
"broadcast", "Keep", "giveAllPlayers", "false", "onlineOnly", "true"), enable.revision());
assertEquals("true", service.readQuickSetup("vote-party", Map.of()).options().get("enabled"));
}
@Test void legacyVotePartyQuickSetupPreservesEnabledWhenOmitted() throws Exception {
Files.writeString(directory.resolve("SpecialRewards.yml"), "VoteParty:\n Enabled: false\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("vote-party", Map.of(
"votesRequired", "25", "command", "", "broadcast", "", "giveAllPlayers", "false",
@@ -937,7 +935,7 @@ class BackendConfigurationServiceTest {
@Test void oversizedInstalledGuidedValuesFailInsteadOfWedgingResultSubmission() throws Exception {
Files.writeString(directory.resolve("SpecialRewards.yml"),
"VoteParty:\n Broadcast: '" + "é".repeat(251) + "'\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertThrows(IOException.class, () -> service.readQuickSetup("vote-party", Map.of()));
}
@@ -946,7 +944,7 @@ class BackendConfigurationServiceTest {
Path config = directory.resolve("Config.yml");
Files.writeString(config, "AutoCreateVoteSites: true\nProcessRewards: false\nOtherSetting: keep\n");
AtomicInteger reloads = new AtomicInteger();
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, reloads::incrementAndGet);
BackendConfigurationService service = new BackendConfigurationService(directory, reloads::incrementAndGet);
BackendConfigurationService.QuickState state = service.readQuickSetup("auto-create-vote-sites", Map.of());
assertEquals("true", state.options().get("enabled"));
@@ -954,7 +952,7 @@ class BackendConfigurationServiceTest {
Map.of("enabled", "false"));
assertEquals(List.of("changed AutoCreateVoteSites"), preview.changes());
BackendConfigurationServiceAccess.applyQuickSetup(service, "auto-create-vote-sites", Map.of("enabled", "false"), state.revision());
service.applyQuickSetup("auto-create-vote-sites", Map.of("enabled", "false"), state.revision());
YamlConfiguration applied = YamlConfiguration.loadConfiguration(config.toFile());
assertFalse(applied.getBoolean("AutoCreateVoteSites"));
assertFalse(applied.getBoolean("ProcessRewards"));
@@ -968,7 +966,7 @@ class BackendConfigurationServiceTest {
Path config = directory.resolve("Config.yml");
Files.writeString(config, "VoteLogging:\n Enabled: false\n PurgeDays: 30\n UseMainMySQL: true\n"
+ " Host: private-db\n Password: keep-secret\nProcessRewards: false\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickState state = service.readQuickSetup("vote-logging", Map.of());
assertEquals(Map.of("enabled", "false", "purgeDays", "30", "useMainMySQL", "true"),
@@ -977,7 +975,7 @@ class BackendConfigurationServiceTest {
"enabled", "true", "purgeDays", "90", "useMainMySQL", "false"));
assertEquals(List.of("changed VoteLogging.Enabled", "changed VoteLogging.PurgeDays",
"changed VoteLogging.UseMainMySQL"), preview.changes());
BackendConfigurationServiceAccess.applyQuickSetup(service, "vote-logging", Map.of("enabled", "true", "purgeDays", "-1",
service.applyQuickSetup("vote-logging", Map.of("enabled", "true", "purgeDays", "-1",
"useMainMySQL", "false"), state.revision());
YamlConfiguration applied = YamlConfiguration.loadConfiguration(config.toFile());
@@ -1002,7 +1000,7 @@ class BackendConfigurationServiceTest {
+ " Commands: [existing]\n Messages:\n Player: Existing message\n");
Files.writeString(directory.resolve("SpecialRewards.yml"), "VoteParty:\n Enabled: true\n"
+ " Rewards:\n Commands: [existing party]\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickPreview reward = service.previewQuickSetup("easy-reward", Map.of(
"scope", "site", "name", "PMC", "command", "new reward", "message", "New message"));
@@ -1024,7 +1022,7 @@ class BackendConfigurationServiceTest {
+ " Commands: [old command]\n Other:\n Rewards:\n Commands: [keep other]\n"
+ "EverySiteReward:\n Commands: [keep every]\n");
AtomicInteger reloads = new AtomicInteger();
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, reloads::incrementAndGet);
BackendConfigurationService service = new BackendConfigurationService(directory, reloads::incrementAndGet);
String encoded = """
{"scope":"site","site":"PMC","commands":["eco give %player% 100"],
"playerMessages":["Thanks"],"broadcastMessages":["%player% voted"],
@@ -1035,7 +1033,7 @@ class BackendConfigurationServiceTest {
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("reward-builder", options);
assertEquals("VoteSites.yml", preview.proposal().fileName());
assertEquals(preview.revision(), BackendConfigurationServiceAccess.currentQuickSetupRevision(service, "reward-builder",
assertEquals(preview.revision(), service.currentQuickSetupRevision("reward-builder",
Map.of("targetFile", "VoteSites.yml")));
YamlConfiguration proposed = new YamlConfiguration();
proposed.loadFromString(preview.proposal().content());
@@ -1056,11 +1054,11 @@ class BackendConfigurationServiceTest {
assertTrue(preview.proposal().content().contains("keep other"));
assertTrue(preview.proposal().content().contains("keep every"));
BackendConfigurationService.ApplyResult applied = BackendConfigurationServiceAccess.applyQuickSetup(service, "reward-builder", options,
BackendConfigurationService.ApplyResult applied = service.applyQuickSetup("reward-builder", options,
preview.revision());
assertEquals(1, reloads.get());
assertTrue(Files.readString(voteSites).contains("eco give %player% 100"));
assertEquals(applied.document().revision(), BackendConfigurationServiceAccess.currentQuickSetupRevision(service, "reward-builder",
assertEquals(applied.document().revision(), service.currentQuickSetupRevision("reward-builder",
Map.of("targetFile", "VoteSites.yml")));
}
@@ -1068,7 +1066,7 @@ class BackendConfigurationServiceTest {
Files.writeString(directory.resolve("VoteSites.yml"), "VoteSites: {}\nEverySiteReward:\n Commands: [old]\n");
Files.writeString(directory.resolve("SpecialRewards.yml"), "VoteParty:\n Enabled: false\n"
+ " VotesRequired: 40\n Rewards:\n Commands: [old party]\nOtherReward: keep\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickPreview every = service.previewQuickSetup("reward-builder", Map.of(
"proposal", "{\"scope\":\"every-site\",\"commands\":[\"say every\"]}"));
@@ -1089,7 +1087,7 @@ class BackendConfigurationServiceTest {
@Test void rewardBuilderRejectsUnknownOptionsMalformedPlansAndMissingSites() throws Exception {
Files.writeString(directory.resolve("VoteSites.yml"), "VoteSites: {}\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertThrows(IllegalArgumentException.class, () -> service.readQuickSetup("reward-builder", Map.of()));
assertThrows(IllegalArgumentException.class, () -> service.previewQuickSetup("reward-builder",
@@ -1101,14 +1099,14 @@ class BackendConfigurationServiceTest {
String oversized = "{\"scope\":\"every-site\",\"commands\":[\"" + "x".repeat(64 * 1024) + "\"]}";
assertThrows(IllegalArgumentException.class, () -> service.previewQuickSetup("reward-builder",
Map.of("proposal", oversized)));
assertThrows(IllegalArgumentException.class, () -> BackendConfigurationServiceAccess.currentQuickSetupRevision(service, "reward-builder",
assertThrows(IllegalArgumentException.class, () -> service.currentQuickSetupRevision("reward-builder",
Map.of("targetFile", "Config.yml")));
}
@Test void proxyBackendQuickSetupRejectsUnknownTransportMethods() throws Exception {
Files.writeString(directory.resolve("BungeeSettings.yml"),
"UseBungeecord: false\nServer: PleaseSet\nBungeeMethod: PLUGINMESSAGING\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertThrows(IllegalArgumentException.class, () -> service.previewQuickSetup("proxy-backend",
Map.of("server", "lobby", "method", "NOT_A_TRANSPORT")));
@@ -1123,7 +1121,7 @@ class BackendConfigurationServiceTest {
@Test void httpConnectionCodeIsRedactedFromManagedConfiguration() throws Exception {
Files.writeString(directory.resolve("BungeeSettings.yml"),
"HTTP:\n ConnectionCode: VPH1-sensitive-enrollment-code\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.Document read = service.read("BungeeSettings.yml");
assertFalse(read.content().contains("VPH1-sensitive-enrollment-code"));
@@ -1142,7 +1140,7 @@ class BackendConfigurationServiceTest {
com.bencodez.simpleapi.servercomm.http.HttpClientCredentialStore.saveEnrolled(directory.resolve("http"), code,
identity.issueClientCertificate("lobby-1"));
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertDoesNotThrow(() -> service.previewQuickSetup("proxy-method", Map.of("method", "HTTP")));
}
@@ -1150,7 +1148,7 @@ class BackendConfigurationServiceTest {
Path settings = directory.resolve("BungeeSettings.yml");
Files.writeString(settings, "UseBungeecord: true\nServer: lobby-1\nBungeeMethod: PLUGINMESSAGING\n"
+ "PluginMessageChannel: vp:vp\nHTTP:\n ConnectionCode: malformed\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertThrows(IllegalArgumentException.class,
() -> service.previewQuickSetup("proxy-method", Map.of("method", "HTTP")));
@@ -1180,7 +1178,7 @@ class BackendConfigurationServiceTest {
+ "MQTT:\n ClientID: lobby\n BrokerURL: bad\nBungeeServer:\n Host: ''\n Port: 1297\n");
Files.writeString(directory.resolve("Config.yml"),
"DataStorage: SQLITE\nDatabase:\n Host: ''\n Database: ''\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertThrows(IllegalArgumentException.class,
() -> service.previewQuickSetup("proxy-method", Map.of("method", "REDIS")));
@@ -1201,7 +1199,7 @@ class BackendConfigurationServiceTest {
@Test void pluginMessagingMethodRequiresBackendIdentityForDiagnostics() throws Exception {
Files.writeString(directory.resolve("BungeeSettings.yml"),
"UseBungeecord: true\nServer: PleaseSet\nBungeeMethod: REDIS\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertThrows(IllegalArgumentException.class, () -> service.previewQuickSetup("proxy-method",
Map.of("method", "PLUGINMESSAGING")));
@@ -1212,11 +1210,11 @@ class BackendConfigurationServiceTest {
Files.writeString(settings, "# Proxy settings\n#\n# Method selection\n"
+ "UseBungeecord: true\nServer: server\nBungeeMethod: PLUGINMESSAGING\n"
+ "PluginMessageChannel: vp:vp\nRedis:\n Host: localhost\n Port: 6379\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("proxy-method",
Map.of("method", "REDIS"));
BackendConfigurationService.ApplyResult applied = BackendConfigurationServiceAccess.applyQuickSetup(service, "proxy-method",
BackendConfigurationService.ApplyResult applied = service.applyQuickSetup("proxy-method",
Map.of("method", "REDIS"), preview.revision(), ignored -> { });
assertFalse(applied.rolledBack());
@@ -1231,11 +1229,11 @@ class BackendConfigurationServiceTest {
+ "PluginMessageChannel: vp:vp\nRedis:\n Host: localhost\n Port: 6379\n");
AtomicInteger fullReloads = new AtomicInteger();
AtomicInteger transportSwitches = new AtomicInteger();
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, fullReloads::incrementAndGet);
BackendConfigurationService service = new BackendConfigurationService(directory, fullReloads::incrementAndGet);
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("proxy-method",
Map.of("method", "REDIS"));
BackendConfigurationServiceAccess.applyQuickSetup(service, "proxy-method", Map.of("method", "REDIS"), preview.revision(),
service.applyQuickSetup("proxy-method", Map.of("method", "REDIS"), preview.revision(),
ignored -> transportSwitches.incrementAndGet());
assertEquals(0, fullReloads.get());
@@ -1249,13 +1247,13 @@ class BackendConfigurationServiceTest {
+ "PluginMessageChannel: vp:vp\nRedis:\n Host: localhost\n Port: 6379\n";
Files.writeString(settings, original);
AtomicInteger targeted = new AtomicInteger();
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("proxy-method",
Map.of("method", "REDIS"));
BackendConfigurationService.ApplyFailureException failure = assertThrows(
BackendConfigurationService.ApplyFailureException.class,
() -> BackendConfigurationServiceAccess.applyQuickSetup(service, "proxy-method", Map.of("method", "REDIS"), preview.revision(), ignored -> {
() -> service.applyQuickSetup("proxy-method", Map.of("method", "REDIS"), preview.revision(), ignored -> {
if (targeted.incrementAndGet() == 1) throw new IOException("transport activation failed");
}));
@@ -1298,12 +1296,12 @@ class BackendConfigurationServiceTest {
+ " Commands: ['source new reward']\n"
+ "EverySiteReward:\n"
+ " Commands: ['source every-site reward']\n";
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.Document before = service.read("VoteSites.yml");
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("sync-vote-sites",
Map.of("sourceContent", source));
String recoveryRevision = BackendConfigurationServiceAccess.proposedQuickSetupRevision(service, "sync-vote-sites", preview);
String recoveryRevision = service.proposedQuickSetupRevision("sync-vote-sites", preview);
YamlConfiguration proposal = new YamlConfiguration();
proposal.options().parseComments(true);
proposal.loadFromString(preview.proposal().content());
@@ -1320,7 +1318,7 @@ class BackendConfigurationServiceTest {
assertTrue(preview.proposal().content().contains("# synchronized field"));
assertTrue(preview.changes().stream().noneMatch(change -> change.toLowerCase().contains("reward")));
BackendConfigurationService.ApplyResult result = BackendConfigurationServiceAccess.applyQuickSetup(service, "sync-vote-sites",
BackendConfigurationService.ApplyResult result = service.applyQuickSetup("sync-vote-sites",
Map.of("sourceContent", source), preview.revision());
assertEquals(recoveryRevision, result.document().revision());
String applied = Files.readString(voteSites);
@@ -1334,7 +1332,7 @@ class BackendConfigurationServiceTest {
@Test void voteSitesSyncMatchesRootSitesAndFieldsCaseInsensitively() throws Exception {
Files.writeString(directory.resolve("Config.yml"), "CaseInsensitiveYMLFiles: true\n");
Files.writeString(directory.resolve("VoteSites.yml"), "votesites:\n pmc:\n name: Target\n Rewards:\n Commands: ['keep']\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("sync-vote-sites", Map.of(
"sourceContent", "VOTESITES:\n PMC:\n Name: Source\n VoteURL: https://example.invalid/vote\n"));
@@ -1352,22 +1350,22 @@ class BackendConfigurationServiceTest {
Path config = directory.resolve("Config.yml");
Files.writeString(config, "CaseInsensitiveYMLFiles: true\nMessage: before\n");
Files.writeString(directory.resolve("VoteSites.yml"), "VoteSites:\n PMC:\n Name: Target\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
Map<String, String> source = Map.of("sourceContent", "VoteSites:\n PMC:\n Name: Source\n");
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("sync-vote-sites", source);
Files.writeString(config, "CaseInsensitiveYMLFiles: true\nMessage: independently edited\n");
assertDoesNotThrow(() -> BackendConfigurationServiceAccess.applyQuickSetup(service, "sync-vote-sites", source, preview.revision()));
assertDoesNotThrow(() -> service.applyQuickSetup("sync-vote-sites", source, preview.revision()));
BackendConfigurationService.QuickPreview policyPreview = service.previewQuickSetup("sync-vote-sites", source);
Files.writeString(config, "CaseInsensitiveYMLFiles: false\nMessage: independently edited\n");
assertThrows(BackendConfigurationService.StaleRevisionException.class,
() -> BackendConfigurationServiceAccess.applyQuickSetup(service, "sync-vote-sites", source, policyPreview.revision()));
() -> service.applyQuickSetup("sync-vote-sites", source, policyPreview.revision()));
}
@Test void voteSitesSyncDropsUnrestorableSourceCredentialComments() throws Exception {
Files.writeString(directory.resolve("VoteSites.yml"), "VoteSites:\n PMC:\n Name: Target\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
String source = "VoteSites:\n # Password: " + BackendConfigurationService.REDACTED + "\n"
+ " # public source note\n PMC:\n Name: Source # Token: "
+ BackendConfigurationService.REDACTED + "\n";
@@ -1377,7 +1375,7 @@ class BackendConfigurationServiceTest {
assertFalse(preview.proposal().content().contains(BackendConfigurationService.REDACTED));
assertTrue(preview.proposal().content().contains("public source note"));
BackendConfigurationService.ApplyResult applied = BackendConfigurationServiceAccess.applyQuickSetup(service, "sync-vote-sites",
BackendConfigurationService.ApplyResult applied = service.applyQuickSetup("sync-vote-sites",
Map.of("sourceContent", source), preview.revision());
YamlConfiguration installed = new YamlConfiguration();
installed.loadFromString(applied.document().content());
@@ -1388,7 +1386,7 @@ class BackendConfigurationServiceTest {
Path voteSites = directory.resolve("VoteSites.yml");
Files.writeString(voteSites, "VoteSites:\n PMC:\n"
+ " Name: Target # Password: target-comment-secret\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
String source = "VoteSites:\n PMC:\n Name: Source # public source comment\n";
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("sync-vote-sites",
@@ -1399,7 +1397,7 @@ class BackendConfigurationServiceTest {
assertThrows(IllegalArgumentException.class, () -> service.apply("VoteSites.yml",
preview.proposal().content(), editorSnapshot.revision()));
BackendConfigurationServiceAccess.applyQuickSetup(service, "sync-vote-sites", Map.of("sourceContent", source), preview.revision());
service.applyQuickSetup("sync-vote-sites", Map.of("sourceContent", source), preview.revision());
String applied = Files.readString(voteSites);
assertTrue(applied.contains("Name: Source"));
assertTrue(applied.contains("target-comment-secret"));
@@ -1409,7 +1407,7 @@ class BackendConfigurationServiceTest {
Files.writeString(directory.resolve("Config.yml"), "caseinsensitiveymlfiles: false\n");
Files.writeString(directory.resolve("VoteSites.yml"), "VoteSites:\n PMC:\n Name: Upper target\n"
+ " pmc:\n Name: Lower target\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("sync-vote-sites", Map.of(
"sourceContent", "votesites:\n PMC:\n name: Updated upper\n"));
@@ -1424,7 +1422,7 @@ class BackendConfigurationServiceTest {
Files.writeString(directory.resolve("Config.yml"), "Message: no casing policy configured\n");
Files.writeString(directory.resolve("VoteSites.yml"), "VoteSites:\n PMC:\n Name: Upper target\n"
+ " pmc:\n Name: Lower target\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("sync-vote-sites", Map.of(
"sourceContent", "VoteSites:\n PMC:\n Name: Updated upper\n"));
@@ -1437,7 +1435,7 @@ class BackendConfigurationServiceTest {
@Test void voteSitesSyncRejectsMalformedOrMissingSourceSections() throws Exception {
Files.writeString(directory.resolve("VoteSites.yml"), "VoteSites: {}\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertThrows(IllegalArgumentException.class, () -> service.previewQuickSetup("sync-vote-sites",
Map.of("sourceContent", "not: [yaml")));
@@ -1449,7 +1447,7 @@ class BackendConfigurationServiceTest {
@Test void voteSitesSyncSkipsRewardOnlySites() throws Exception {
Files.writeString(directory.resolve("VoteSites.yml"), "VoteSites:\n Existing:\n Name: Existing\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("sync-vote-sites", Map.of(
"sourceContent", "VoteSites:\n RewardOnly:\n Rewards:\n Commands: ['say ignored']\n"));
@@ -1462,7 +1460,7 @@ class BackendConfigurationServiceTest {
@Test void voteSitesSyncPreservesTargetsBelowRewardOnlyWrappers() throws Exception {
Files.writeString(directory.resolve("VoteSites.yml"),
"VoteSites:\n Example:\n Name: Target\n Metadata: keep\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("sync-vote-sites", Map.of(
"sourceContent", "VoteSites:\n Example:\n Name: Source\n Metadata:\n"
@@ -1477,7 +1475,7 @@ class BackendConfigurationServiceTest {
@Test void voteSitesSyncDoesNotReplaceTargetSectionsContainingRewardDescendants() throws Exception {
Files.writeString(directory.resolve("VoteSites.yml"), "VoteSites:\n Example:\n Name: Target\n"
+ " Metadata:\n Owner: keep\n Rewards:\n Commands: ['target reward']\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.QuickPreview preview = service.previewQuickSetup("sync-vote-sites", Map.of(
"sourceContent", "VoteSites:\n Example:\n Name: Source\n Metadata: source scalar\n"));
@@ -1493,7 +1491,7 @@ class BackendConfigurationServiceTest {
@Test void fullBungeeSettingsRejectsUnknownTransportMethods() throws Exception {
Files.writeString(directory.resolve("BungeeSettings.yml"),
"UseBungeecord: true\nServer: lobby\nBungeeMethod: PLUGINMESSAGING\n");
BackendConfigurationService service = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService service = new BackendConfigurationService(directory, () -> { });
assertThrows(IllegalArgumentException.class, () -> service.preview("BungeeSettings.yml",
"UseBungeecord: true\nServer: lobby\nBungeeMethod: NOT_A_TRANSPORT\n"));
@@ -1,7 +1,4 @@
package com.bencodez.votingplugin.tests.control;
import com.bencodez.votingplugin.control.BackendControlAutoEnrollment;
import com.bencodez.votingplugin.control.ControlEnrollmentAuthenticator;
package com.bencodez.votingplugin.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -123,7 +120,7 @@ class BackendControlAutoEnrollmentTest {
"backend-a", requestId, false, challenge, resultProof));
verify(plugin).startBackendControlConnectorForEnrollment(enrollment);
org.mockito.Mockito.clearInvocations(messages);
BackendControlAutoEnrollmentAccess.send(enrollment);
enrollment.send();
ArgumentCaptor<JsonEnvelope> provedRequest = ArgumentCaptor.forClass(JsonEnvelope.class);
verify(messages).sendMessage(provedRequest.capture());
@@ -145,7 +142,7 @@ class BackendControlAutoEnrollmentTest {
"backend-a", requestId, false, challenge, resultProof));
verify(plugin, times(2)).startBackendControlConnectorForEnrollment(enrollment);
org.mockito.Mockito.clearInvocations(messages);
BackendControlAutoEnrollmentAccess.send(enrollment);
enrollment.send();
ArgumentCaptor<JsonEnvelope> retriedRequest = ArgumentCaptor.forClass(JsonEnvelope.class);
verify(messages).sendMessage(retriedRequest.capture());
VotingPluginWire.ControlEnrollmentRequest retried =
@@ -1,10 +1,4 @@
package com.bencodez.votingplugin.tests.control;
import com.bencodez.votingplugin.control.BackendConfigurationService;
import com.bencodez.votingplugin.control.BackendControlConnector;
import com.bencodez.votingplugin.tests.control.BackendControlResultStoreAccess;
import com.bencodez.votingplugin.control.ControlInspectionService;
import com.bencodez.votingplugin.control.PluginDeploymentService;
package com.bencodez.votingplugin.control;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -29,7 +23,7 @@ import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import com.bencodez.votingplugin.tests.control.BackendControlResultStoreAccess.StoredResult;
import com.bencodez.votingplugin.control.BackendControlResultStore.StoredResult;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
@@ -65,7 +59,7 @@ class BackendControlConnectorProtocolTest {
field.setAccessible(true);
assertNull(field.get(connector));
JsonObject body = new JsonObject();
BackendControlConnectorAccess.addCapabilities(body, false, field.get(connector) != null);
BackendControlConnector.addCapabilities(body, false, field.get(connector) != null);
assertFalse(body.getAsJsonArray("capabilities").asList().stream()
.anyMatch(value -> PluginDeploymentService.CAPABILITY.equals(value.getAsString())));
org.mockito.Mockito.verify(plugin, org.mockito.Mockito.never()).getServer();
@@ -74,21 +68,21 @@ class BackendControlConnectorProtocolTest {
}
@Test void onlyTheLeaseExpiryConflictRequestsAResultReclaim() {
assertTrue(BackendControlConnectorAccess.taskLeaseExpired(new BackendControlConnectorAccess.Response(409,
assertTrue(BackendControlConnector.taskLeaseExpired(new BackendControlConnector.Response(409,
"{\"error\":{\"code\":\"TASK_LEASE_EXPIRED\"}}")));
assertFalse(BackendControlConnectorAccess.taskLeaseExpired(new BackendControlConnectorAccess.Response(409,
assertFalse(BackendControlConnector.taskLeaseExpired(new BackendControlConnector.Response(409,
"{\"error\":{\"code\":\"SESSION_MISMATCH\"}}")));
assertFalse(BackendControlConnectorAccess.taskLeaseExpired(new BackendControlConnectorAccess.Response(500, "not-json")));
assertTrue(BackendControlConnectorAccess.operationNotFound(new BackendControlConnectorAccess.Response(404,
assertFalse(BackendControlConnector.taskLeaseExpired(new BackendControlConnector.Response(500, "not-json")));
assertTrue(BackendControlConnector.operationNotFound(new BackendControlConnector.Response(404,
"{\"error\":{\"code\":\"OPERATION_NOT_FOUND\"}}")));
assertFalse(BackendControlConnectorAccess.operationNotFound(new BackendControlConnectorAccess.Response(404,
assertFalse(BackendControlConnector.operationNotFound(new BackendControlConnector.Response(404,
"{\"error\":{\"code\":\"NODE_NOT_FOUND\"}}")));
}
@Test void deploymentTaskRejectsUnknownV1Fields() {
JsonObject task = deploymentTask();
task.addProperty("unexpected", "value");
assertThrows(IllegalArgumentException.class, () -> BackendControlConnectorAccess.deploymentTask(task));
assertThrows(IllegalArgumentException.class, () -> BackendControlConnector.deploymentTask(task));
}
private static JsonObject deploymentTask() {
@@ -105,7 +99,7 @@ class BackendControlConnectorProtocolTest {
JsonObject anticipated = new JsonObject();
anticipated.addProperty("attemptId", "00000000-0000-0000-0000-000000000199");
StoredResult recovered = BackendControlConnectorAccess.abortedIntent(
StoredResult recovered = BackendControlConnector.abortedIntent(
new StoredResult(anticipated, true, false, false));
assertTrue(recovered.committed());
@@ -116,7 +110,7 @@ class BackendControlConnectorProtocolTest {
@Test void installedFileIntentRecoveryRebuildsMaskedContent() throws Exception {
Files.writeString(directory.resolve("Config.yml"), "Database:\n Password: keep-me\nDebug: true\n");
BackendConfigurationService configurations = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService configurations = new BackendConfigurationService(directory, () -> { });
BackendConfigurationService.Document installed = configurations.read("Config.yml");
JsonObject configuration = new JsonObject();
configuration.addProperty("domain", "file");
@@ -124,7 +118,7 @@ class BackendControlConnectorProtocolTest {
JsonObject intent = new JsonObject();
intent.addProperty("revision", installed.revision());
intent.add("configuration", configuration);
StoredResult recovered = BackendControlConnectorAccess.committedInstalledForAttempt(configurations,
StoredResult recovered = BackendControlConnector.committedInstalledForAttempt(configurations,
new StoredResult(intent, false, false, false), "00000000-0000-0000-0000-000000000198");
assertTrue(recovered.committed());
@@ -135,7 +129,7 @@ class BackendControlConnectorProtocolTest {
}
@Test void changedMalformedFilesAbortPendingRecoveryBeforeYamlParsing() throws Exception {
BackendConfigurationService configurations = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService configurations = new BackendConfigurationService(directory, () -> { });
for (String fileName : List.of("Config.yml", "Rewards/Daily.yml")) {
Path file = directory.resolve(fileName);
Files.createDirectories(file.getParent());
@@ -151,10 +145,10 @@ class BackendControlConnectorProtocolTest {
StoredResult pending = new StoredResult(intent, false, false, false);
Files.writeString(file, "Money: [\n");
assertThrows(IOException.class, () -> configurations.read(fileName));
assertNull(BackendControlConnectorAccess.committedInstalledForAttempt(configurations, pending, "attempt"));
assertTrue(BackendControlConnectorAccess.abortedIntent(pending).committed());
assertNull(BackendControlConnector.committedInstalledForAttempt(configurations, pending, "attempt"));
assertTrue(BackendControlConnector.abortedIntent(pending).committed());
Files.writeString(file, "Money: 1\n");
assertEquals(revision, BackendControlConnectorAccess.committedInstalledForAttempt(configurations,
assertEquals(revision, BackendControlConnector.committedInstalledForAttempt(configurations,
pending, "attempt").result().get("revision").getAsString());
}
}
@@ -163,7 +157,7 @@ class BackendControlConnectorProtocolTest {
Path rewards = Files.createDirectory(directory.resolve("Rewards"));
Path daily = rewards.resolve("Daily.yml");
Files.writeString(daily, "Money: 1\n");
BackendConfigurationService configurations = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService configurations = new BackendConfigurationService(directory, () -> { });
String revision = configurations.read("Rewards/Daily.yml").revision();
JsonObject configuration = new JsonObject();
configuration.addProperty("domain", "file");
@@ -174,25 +168,25 @@ class BackendControlConnectorProtocolTest {
intent.add("configuration", configuration);
StoredResult pending = new StoredResult(intent, false, false, false);
Files.delete(daily);
assertNull(BackendControlConnectorAccess.committedInstalledForAttempt(configurations, pending, "attempt"));
assertTrue(BackendControlConnectorAccess.abortedIntent(pending).committed());
assertNull(BackendControlConnector.committedInstalledForAttempt(configurations, pending, "attempt"));
assertTrue(BackendControlConnector.abortedIntent(pending).committed());
Files.delete(rewards);
assertNull(BackendControlConnectorAccess.committedInstalledForAttempt(configurations, pending, "attempt"));
assertNull(BackendControlConnector.committedInstalledForAttempt(configurations, pending, "attempt"));
Files.createDirectory(rewards);
Files.createSymbolicLink(daily, directory.resolve("not-a-reward.yml"));
assertNull(BackendControlConnectorAccess.committedInstalledForAttempt(configurations, pending, "attempt"));
assertNull(BackendControlConnector.committedInstalledForAttempt(configurations, pending, "attempt"));
Files.delete(daily);
Files.createDirectory(daily);
assertNull(BackendControlConnectorAccess.committedInstalledForAttempt(configurations, pending, "attempt"));
assertNull(BackendControlConnector.committedInstalledForAttempt(configurations, pending, "attempt"));
Files.delete(daily);
Files.writeString(daily, "x".repeat(BackendConfigurationService.MAX_CONTENT_BYTES + 1));
assertNull(BackendControlConnectorAccess.committedInstalledForAttempt(configurations, pending, "attempt"));
assertNull(BackendControlConnector.committedInstalledForAttempt(configurations, pending, "attempt"));
Files.write(daily, new byte[] {(byte) 0xC3, (byte) 0x28});
assertNull(BackendControlConnectorAccess.committedInstalledForAttempt(configurations, pending, "attempt"));
assertNull(BackendControlConnector.committedInstalledForAttempt(configurations, pending, "attempt"));
Files.writeString(daily, "Money: 1\n");
assertEquals(revision, BackendControlConnectorAccess.committedInstalledForAttempt(configurations,
assertEquals(revision, BackendControlConnector.committedInstalledForAttempt(configurations,
pending, "attempt").result().get("revision").getAsString());
assertFalse(BackendConfigurationServiceAccess.namedRewardCannotBeConfirmed(new IOException("transient read failure")));
assertFalse(BackendConfigurationService.namedRewardCannotBeConfirmed(new IOException("transient read failure")));
}
@Test void fifoNamedRewardIntentTerminatesWithoutOpeningItsBody() throws Exception {
@@ -201,7 +195,7 @@ class BackendControlConnectorProtocolTest {
Path rewards = Files.createDirectory(directory.resolve("Rewards"));
Path daily = rewards.resolve("Daily.yml");
Files.writeString(daily, "Money: 1\n");
BackendConfigurationService configurations = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService configurations = new BackendConfigurationService(directory, () -> { });
String revision = configurations.read("Rewards/Daily.yml").revision();
JsonObject configuration = new JsonObject();
configuration.addProperty("domain", "file");
@@ -213,14 +207,14 @@ class BackendControlConnectorProtocolTest {
Process create = new ProcessBuilder("/usr/bin/mkfifo", daily.toString()).start();
assertTrue(create.waitFor(5, TimeUnit.SECONDS));
assertEquals(0, create.exitValue());
assertNull(BackendControlConnectorAccess.committedInstalledForAttempt(configurations,
assertNull(BackendControlConnector.committedInstalledForAttempt(configurations,
new StoredResult(intent, false, false, false), "attempt"));
}
@Test void namedRewardRecoveryIgnoresUnrelatedInvalidInventoryEntries() throws Exception {
Path rewards = Files.createDirectory(directory.resolve("Rewards"));
Files.writeString(rewards.resolve("Daily.yml"), "Money: 1\n");
BackendConfigurationService configurations = BackendConfigurationServiceAccess.create(directory, () -> { });
BackendConfigurationService configurations = new BackendConfigurationService(directory, () -> { });
String revision = configurations.read("Rewards/Daily.yml").revision();
JsonObject configuration = new JsonObject();
configuration.addProperty("domain", "file");
@@ -234,21 +228,21 @@ class BackendControlConnectorProtocolTest {
Path unsafe = rewards.resolve("Unrelated.yml");
Files.createSymbolicLink(unsafe, directory.resolve("outside.yml"));
assertThrows(IOException.class, () -> configurations.read("Rewards/Daily.yml"));
assertEquals(revision, BackendControlConnectorAccess.committedInstalledForAttempt(configurations,
assertEquals(revision, BackendControlConnector.committedInstalledForAttempt(configurations,
pending, "attempt").result().get("revision").getAsString());
Files.delete(unsafe);
Path ambiguous = rewards.resolve("daily.yml");
Files.writeString(ambiguous, "Money: 2\n");
assertThrows(IOException.class, () -> configurations.read("Rewards/Daily.yml"));
assertNull(BackendControlConnectorAccess.committedInstalledForAttempt(configurations, pending, "attempt"));
assertTrue(BackendControlConnectorAccess.abortedIntent(pending).committed());
assertNull(BackendControlConnector.committedInstalledForAttempt(configurations, pending, "attempt"));
assertTrue(BackendControlConnector.abortedIntent(pending).committed());
Files.delete(ambiguous);
Path oversized = rewards.resolve("Oversized.yml");
Files.writeString(oversized, "x".repeat(BackendConfigurationService.MAX_CONTENT_BYTES + 1));
assertThrows(IOException.class, () -> configurations.read("Rewards/Daily.yml"));
assertEquals(revision, BackendControlConnectorAccess.committedInstalledForAttempt(configurations,
assertEquals(revision, BackendControlConnector.committedInstalledForAttempt(configurations,
pending, "attempt").result().get("revision").getAsString());
Files.delete(oversized);
@@ -256,18 +250,18 @@ class BackendControlConnectorProtocolTest {
Files.writeString(rewards.resolve("Extra" + index + ".yml"), "Money: 0\n");
}
assertThrows(IOException.class, () -> configurations.read("Rewards/Daily.yml"));
assertEquals(revision, BackendControlConnectorAccess.committedInstalledForAttempt(configurations,
assertEquals(revision, BackendControlConnector.committedInstalledForAttempt(configurations,
pending, "attempt").result().get("revision").getAsString());
}
@Test void registrationRequiresFileControlButAllowsQuickSetupToRemainOptional() {
assertThrows(RuntimeException.class, () -> BackendControlConnectorAccess.requireFileCapability(false));
assertDoesNotThrow(() -> BackendControlConnectorAccess.requireFileCapability(true));
assertThrows(RuntimeException.class, () -> BackendControlConnector.requireFileCapability(false));
assertDoesNotThrow(() -> BackendControlConnector.requireFileCapability(true));
}
@Test void registrationAdvertisesCommentPreservingFilesAsAnOptionalCapability() {
JsonObject registration = new JsonObject();
BackendControlConnectorAccess.addCapabilities(registration, true, false);
BackendControlConnector.addCapabilities(registration, true, false);
JsonArray advertised = registration.getAsJsonArray("capabilities");
assertTrue(advertised.asList().stream()
@@ -293,12 +287,12 @@ class BackendControlConnectorProtocolTest {
@Test void deploymentCapabilityIsOnlyAddedWhenStagingWasPrepared() {
JsonObject unavailable = new JsonObject();
BackendControlConnectorAccess.addCapabilities(unavailable, true, false);
BackendControlConnector.addCapabilities(unavailable, true, false);
assertFalse(unavailable.getAsJsonArray("capabilities").asList().stream()
.anyMatch(value -> PluginDeploymentService.CAPABILITY.equals(value.getAsString())));
JsonObject ready = new JsonObject();
BackendControlConnectorAccess.addCapabilities(ready, true, true);
BackendControlConnector.addCapabilities(ready, true, true);
assertTrue(ready.getAsJsonArray("capabilities").asList().stream()
.anyMatch(value -> PluginDeploymentService.CAPABILITY.equals(value.getAsString())));
assertTrue(ready.getAsJsonArray("capabilities").asList().stream()
@@ -307,7 +301,7 @@ class BackendControlConnectorProtocolTest {
@Test void unsupportedFilesystemDoesNotAdvertiseNamedRewards() {
JsonObject registration = new JsonObject();
BackendControlConnectorAccess.addCapabilities(registration, false, true);
BackendControlConnector.addCapabilities(registration, false, true);
assertFalse(registration.getAsJsonArray("capabilities").asList().stream()
.anyMatch(value -> "config.reward-files.v1".equals(value.getAsString())));
assertTrue(registration.getAsJsonArray("capabilities").asList().stream()
@@ -318,36 +312,36 @@ class BackendControlConnectorProtocolTest {
@Test void heartbeatRetainsOmittedCapabilitiesAndHonorsExplicitReplacement() {
JsonObject omitted = new JsonObject();
assertTrue(BackendControlConnectorAccess.negotiatedCapability(omitted, "config.files.v1", true));
assertTrue(BackendControlConnector.negotiatedCapability(omitted, "config.files.v1", true));
JsonObject explicit = new JsonObject();
JsonArray accepted = new JsonArray();
accepted.add("config.quick-setup.v1");
explicit.add("acceptedCapabilities", accepted);
assertFalse(BackendControlConnectorAccess.negotiatedCapability(explicit, "config.files.v1", true));
assertTrue(BackendControlConnectorAccess.negotiatedCapability(explicit, "config.quick-setup.v1", false));
assertFalse(BackendControlConnector.negotiatedCapability(explicit, "config.files.v1", true));
assertTrue(BackendControlConnector.negotiatedCapability(explicit, "config.quick-setup.v1", false));
}
@Test void repeatedInspectionFailuresUseBoundedExponentialBackoff() {
assertEquals(0, BackendControlConnectorAccess.inspectionRetryDelayMillis(0));
assertEquals(1000, BackendControlConnectorAccess.inspectionRetryDelayMillis(1));
assertEquals(2000, BackendControlConnectorAccess.inspectionRetryDelayMillis(2));
assertEquals(256000, BackendControlConnectorAccess.inspectionRetryDelayMillis(9));
assertEquals(300000, BackendControlConnectorAccess.inspectionRetryDelayMillis(10));
assertEquals(300000, BackendControlConnectorAccess.inspectionRetryDelayMillis(30));
assertEquals(0, BackendControlConnector.inspectionRetryDelayMillis(0));
assertEquals(1000, BackendControlConnector.inspectionRetryDelayMillis(1));
assertEquals(2000, BackendControlConnector.inspectionRetryDelayMillis(2));
assertEquals(256000, BackendControlConnector.inspectionRetryDelayMillis(9));
assertEquals(300000, BackendControlConnector.inspectionRetryDelayMillis(10));
assertEquals(300000, BackendControlConnector.inspectionRetryDelayMillis(30));
}
@Test void voteSitesSyncRequiresBothNegotiatedCapabilities() {
assertFalse(BackendControlConnectorAccess.quickSetupCapabilityAccepted("sync-vote-sites", true, false, false));
assertFalse(BackendControlConnectorAccess.quickSetupCapabilityAccepted("sync-vote-sites", false, false, true));
assertTrue(BackendControlConnectorAccess.quickSetupCapabilityAccepted("sync-vote-sites", true, false, true));
assertTrue(BackendControlConnectorAccess.quickSetupCapabilityAccepted("common-settings", true, false, false));
assertFalse(BackendControlConnector.quickSetupCapabilityAccepted("sync-vote-sites", true, false, false));
assertFalse(BackendControlConnector.quickSetupCapabilityAccepted("sync-vote-sites", false, false, true));
assertTrue(BackendControlConnector.quickSetupCapabilityAccepted("sync-vote-sites", true, false, true));
assertTrue(BackendControlConnector.quickSetupCapabilityAccepted("common-settings", true, false, false));
}
@Test void httpProxyMethodApplyRequiresNegotiatedV2Capability() {
assertFalse(BackendControlConnectorAccess.proxyMethodApplyCapabilityAccepted("HTTP", false));
assertTrue(BackendControlConnectorAccess.proxyMethodApplyCapabilityAccepted("HTTP", true));
assertTrue(BackendControlConnectorAccess.proxyMethodApplyCapabilityAccepted("PLUGINMESSAGING", false));
assertFalse(BackendControlConnector.proxyMethodApplyCapabilityAccepted("HTTP", false));
assertTrue(BackendControlConnector.proxyMethodApplyCapabilityAccepted("HTTP", true));
assertTrue(BackendControlConnector.proxyMethodApplyCapabilityAccepted("PLUGINMESSAGING", false));
}
@Test void httpQuickRequestsRejectMissingV2BeforeTouchingConfiguration() throws Exception {
@@ -374,30 +368,30 @@ class BackendControlConnectorProtocolTest {
}
@Test void votePartyRequiresItsVersionedCapability() {
assertFalse(BackendControlConnectorAccess.quickSetupCapabilityAccepted("vote-party", true, false, false));
assertFalse(BackendControlConnectorAccess.quickSetupCapabilityAccepted("vote-party", false, true, false));
assertTrue(BackendControlConnectorAccess.quickSetupCapabilityAccepted("vote-party", true, true, false));
assertFalse(BackendControlConnector.quickSetupCapabilityAccepted("vote-party", true, false, false));
assertFalse(BackendControlConnector.quickSetupCapabilityAccepted("vote-party", false, true, false));
assertTrue(BackendControlConnector.quickSetupCapabilityAccepted("vote-party", true, true, false));
}
@Test void legacyVotePartyOptionsUseV1ButEnabledRequiresV2() {
assertTrue(BackendControlConnectorAccess.quickSetupCapabilityAccepted("vote-party", true, false, false,
assertTrue(BackendControlConnector.quickSetupCapabilityAccepted("vote-party", true, false, false,
Map.of("threshold", "10")));
assertFalse(BackendControlConnectorAccess.quickSetupCapabilityAccepted("vote-party", true, false, false,
assertFalse(BackendControlConnector.quickSetupCapabilityAccepted("vote-party", true, false, false,
Map.of("enabled", "true")));
assertFalse(BackendControlConnectorAccess.quickSetupCapabilityAccepted("vote-party", false, true, false,
assertFalse(BackendControlConnector.quickSetupCapabilityAccepted("vote-party", false, true, false,
Map.of("enabled", "true")));
assertTrue(BackendControlConnectorAccess.quickSetupCapabilityAccepted("vote-party", true, true, false,
assertTrue(BackendControlConnector.quickSetupCapabilityAccepted("vote-party", true, true, false,
Map.of("enabled", "true")));
Map<String, String> state = Map.of("enabled", "false", "votesRequired", "20");
assertEquals(Map.of("votesRequired", "20"),
BackendControlConnectorAccess.resultQuickReadOptions("vote-party", state, Map.of()));
assertEquals(state, BackendControlConnectorAccess.resultQuickReadOptions("vote-party", state,
BackendControlConnector.resultQuickReadOptions("vote-party", state, Map.of()));
assertEquals(state, BackendControlConnector.resultQuickReadOptions("vote-party", state,
Map.of("enabled", "false")));
}
@Test void rewardBuilderResultsKeepOnlyTheSafeRecoveryTarget() {
String proposal = "{\"scope\":\"site\",\"site\":\"PMC\",\"commands\":[\"secret command\"]}";
Map<String, String> result = BackendControlConnectorAccess.resultQuickOptions("reward-builder",
Map<String, String> result = BackendControlConnector.resultQuickOptions("reward-builder",
Map.of("proposal", proposal));
assertEquals(Map.of("targetFile", "VoteSites.yml"), result);
@@ -409,40 +403,40 @@ class BackendControlConnectorProtocolTest {
"/srv/private/VoteSites.yml jdbc:mysql://database.internal user=secret");
assertEquals("Configuration file is unavailable or unreadable",
BackendControlConnectorAccess.operationFailureMessage("READ", failure));
BackendControlConnector.operationFailureMessage("READ", failure));
assertEquals("The backend could not prepare a configuration preview",
BackendControlConnectorAccess.operationFailureMessage("PREVIEW", failure));
BackendControlConnector.operationFailureMessage("PREVIEW", failure));
assertEquals("The backend could not apply the managed configuration",
BackendControlConnectorAccess.operationFailureMessage("APPLY", failure));
assertFalse(BackendControlConnectorAccess.operationFailureMessage("READ", failure).contains("/srv"));
assertEquals("CONFIGURATION_MISSING", BackendControlConnectorAccess.operationFailureCode("READ",
BackendControlConnector.operationFailureMessage("APPLY", failure));
assertFalse(BackendControlConnector.operationFailureMessage("READ", failure).contains("/srv"));
assertEquals("CONFIGURATION_MISSING", BackendControlConnector.operationFailureCode("READ",
new java.nio.file.NoSuchFileException("/srv/private/Config.yml")));
assertEquals("CONFIGURATION_UNREADABLE", BackendControlConnectorAccess.operationFailureCode("READ",
assertEquals("CONFIGURATION_UNREADABLE", BackendControlConnector.operationFailureCode("READ",
new java.nio.file.AccessDeniedException("/srv/private/Config.yml")));
}
@Test void unavailableConfigurationReadsPreserveV1CodeAndUseASafeReason() {
// Existing config.files.v1 peers retain the negotiated code while the
// message gives a useful reason without exposing sensitive paths.
assertEquals("READ_FAILED", BackendControlConnectorAccess.operationFailureCode("READ",
assertEquals("READ_FAILED", BackendControlConnector.operationFailureCode("READ",
new IOException("/srv/private/Config.yml")));
assertEquals("Configuration file is unavailable or unreadable",
BackendControlConnectorAccess.operationFailureMessage("READ", new IOException("/srv/private/Config.yml")));
BackendControlConnector.operationFailureMessage("READ", new IOException("/srv/private/Config.yml")));
}
@Test void networkHealthRequiresItsExplicitCapability() {
BackendControlConnector connector = org.mockito.Mockito.mock(BackendControlConnector.class, org.mockito.Mockito.CALLS_REAL_METHODS);
assertEquals("UNAVAILABLE", BackendControlConnectorAccess.executeInspection(connector, JsonParser.parseString("{\"kind\":\"network-health\",\"filters\":{}}").getAsJsonObject()).code());
assertEquals("UNAVAILABLE", connector.executeInspection(JsonParser.parseString("{\"kind\":\"network-health\",\"filters\":{}}").getAsJsonObject()).code());
}
@Test void namedRewardInventoryRequiresItsExplicitCapability() {
assertTrue(ControlInspectionServiceAccess.rewardFileInventoryQuery(JsonParser.parseString(
assertTrue(ControlInspectionService.rewardFileInventoryQuery(JsonParser.parseString(
"{\"kind\":\"reward-file-inventory\"}").getAsJsonObject()));
assertFalse(ControlInspectionServiceAccess.rewardFileInventoryQuery(JsonParser.parseString(
assertFalse(ControlInspectionService.rewardFileInventoryQuery(JsonParser.parseString(
"{\"kind\":\"overview\"}").getAsJsonObject()));
BackendControlConnector connector = org.mockito.Mockito.mock(BackendControlConnector.class,
org.mockito.Mockito.CALLS_REAL_METHODS);
assertEquals("UNAVAILABLE", BackendControlConnectorAccess.executeInspection(connector, JsonParser.parseString(
assertEquals("UNAVAILABLE", connector.executeInspection(JsonParser.parseString(
"{\"kind\":\"reward-file-inventory\"}").getAsJsonObject()).code());
}
@@ -451,15 +445,15 @@ class BackendControlConnectorProtocolTest {
org.mockito.Mockito.CALLS_REAL_METHODS);
var inspections = BackendControlConnector.class.getDeclaredField("inspections");
inspections.setAccessible(true);
inspections.set(connector, ControlInspectionServiceAccess.create());
inspections.set(connector, new ControlInspectionService());
for (String kind : List.of("null", "{}", "[]")) {
JsonObject query = JsonParser.parseString("{\"kind\":" + kind + ",\"filters\":{}}").getAsJsonObject();
assertEquals("VALIDATION_ERROR", BackendControlConnectorAccess.executeInspection(connector, query).code(), kind);
assertEquals("VALIDATION_ERROR", connector.executeInspection(query).code(), kind);
}
}
@Test void unexpectedInspectionFailureMessagesNeverExposeTheCause() {
String message = BackendControlConnectorAccess.inspectionFailureMessage(new IllegalStateException(
String message = BackendControlConnector.inspectionFailureMessage(new IllegalStateException(
"jdbc:mysql://database.internal/votes user=secret path=/srv/private"));
assertEquals("Inspection failed; see the backend log", message);
@@ -469,7 +463,7 @@ class BackendControlConnectorProtocolTest {
}
@Test void failureMessagesAreSingleLineAndBoundedBeforeSubmission() {
String message = BackendControlConnectorAccess.boundedResultMessage(
String message = BackendControlConnector.boundedResultMessage(
"unsupported field " + "x".repeat(1000) + "\r\nnext line");
assertTrue(message.length() <= 240);
@@ -477,7 +471,7 @@ class BackendControlConnectorProtocolTest {
assertFalse(message.contains("\n"));
assertTrue(message.endsWith("..."));
var changes = BackendControlConnectorAccess.boundedResultChanges(java.util.stream.IntStream.range(0, 25)
var changes = BackendControlConnector.boundedResultChanges(java.util.stream.IntStream.range(0, 25)
.mapToObj(index -> "change-" + index + "-" + "y".repeat(1000)).toList());
assertEquals(20, changes.size());
assertTrue(changes.stream().allMatch(change -> change.length() <= 240));
@@ -485,16 +479,16 @@ class BackendControlConnectorProtocolTest {
}
@Test void operationFailureCodesMatchTheRequestedAction() {
assertEquals("READ_FAILED", BackendControlConnectorAccess.operationFailureCode("READ"));
assertEquals("PREVIEW_FAILED", BackendControlConnectorAccess.operationFailureCode("PREVIEW"));
assertEquals("APPLY_FAILED", BackendControlConnectorAccess.operationFailureCode("APPLY"));
assertEquals("READ_FAILED", BackendControlConnector.operationFailureCode("READ"));
assertEquals("PREVIEW_FAILED", BackendControlConnector.operationFailureCode("PREVIEW"));
assertEquals("APPLY_FAILED", BackendControlConnector.operationFailureCode("APPLY"));
}
@Test void shutdownWaitsForTheClaimedBackendOperation() throws Exception {
var executor = Executors.newSingleThreadScheduledExecutor();
CompletableFuture<Void> operation = new CompletableFuture<>();
CompletableFuture<Void> closing = CompletableFuture.runAsync(
() -> BackendControlConnectorAccess.awaitShutdown(executor, operation));
() -> BackendControlConnector.awaitShutdown(executor, operation));
assertThrows(java.util.concurrent.TimeoutException.class, () -> closing.get(100, TimeUnit.MILLISECONDS));
operation.complete(null);
@@ -517,19 +511,19 @@ class BackendControlConnectorProtocolTest {
});
assertTrue(started.await(1, TimeUnit.SECONDS));
assertTrue(BackendControlConnectorAccess.awaitInspectionShutdown(executor));
assertTrue(BackendControlConnector.awaitInspectionShutdown(executor));
assertTrue(interrupted.get());
assertTrue(executor.isTerminated());
}
@Test void failedResultAcknowledgementDoesNotTriggerConnectorHandoff() throws Exception {
AtomicBoolean handedOff = new AtomicBoolean();
assertThrows(IOException.class, () -> BackendControlConnectorAccess.afterResultAcknowledged(
assertThrows(IOException.class, () -> BackendControlConnector.afterResultAcknowledged(
() -> { throw new IOException("Control result was not acknowledged"); },
() -> handedOff.set(true)));
assertFalse(handedOff.get());
BackendControlConnectorAccess.afterResultAcknowledged(() -> { }, () -> handedOff.set(true));
BackendControlConnector.afterResultAcknowledged(() -> { }, () -> handedOff.set(true));
assertTrue(handedOff.get());
}
}
@@ -1,7 +1,4 @@
package com.bencodez.votingplugin.tests.control;
import com.bencodez.votingplugin.control.BackendConfigurationService;
import com.bencodez.votingplugin.tests.control.BackendControlResultStoreAccess;
package com.bencodez.votingplugin.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -18,8 +15,8 @@ import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import com.bencodez.votingplugin.tests.control.BackendControlResultStoreAccess.Route;
import com.bencodez.votingplugin.tests.control.BackendControlResultStoreAccess.StoredResult;
import com.bencodez.votingplugin.control.BackendControlResultStore.Route;
import com.bencodez.votingplugin.control.BackendControlResultStore.StoredResult;
import com.bencodez.votingplugin.proxy.control.HostedControlManager.HostConfiguration;
import com.google.gson.JsonObject;
@@ -38,14 +35,14 @@ class BackendControlResultStoreTest {
Map<UUID, StoredResult> pending = new LinkedHashMap<>();
pending.put(operationId, new StoredResult(result, true, true, false));
BackendControlResultStoreAccess.save(directory, route, hosted, pending);
BackendControlResultStoreAccess.State recovered = BackendControlResultStoreAccess.load(directory);
BackendControlResultStore.save(directory, route, hosted, pending);
BackendControlResultStore.State recovered = BackendControlResultStore.load(directory);
assertEquals(route, recovered.route());
assertEquals(hosted, recovered.hostedConfiguration());
assertEquals("applied-revision", recovered.results().get(operationId).result().get("revision").getAsString());
assertTrue(recovered.results().get(operationId).committed());
BackendControlResultStoreAccess.save(directory, route, hosted, Map.of());
BackendControlResultStore.save(directory, route, hosted, Map.of());
assertFalse(Files.exists(directory.resolve(".control-pending-results.json")));
}
@@ -56,10 +53,10 @@ class BackendControlResultStoreTest {
JsonObject result = new JsonObject();
result.addProperty("revision", "anticipated-revision");
BackendControlResultStoreAccess.save(directory, route, hosted,
BackendControlResultStore.save(directory, route, hosted,
Map.of(operationId, new StoredResult(result, false, false, false)));
StoredResult recovered = BackendControlResultStoreAccess.load(directory).results().get(operationId);
StoredResult recovered = BackendControlResultStore.load(directory).results().get(operationId);
assertFalse(recovered.committed());
assertFalse(recovered.claimRequired());
}
@@ -88,7 +85,7 @@ class BackendControlResultStoreTest {
root.add("results", results);
Files.writeString(directory.resolve(".control-pending-results.json"), root.toString());
BackendControlResultStoreAccess.State recovered = BackendControlResultStoreAccess.load(directory);
BackendControlResultStore.State recovered = BackendControlResultStore.load(directory);
assertEquals(URI.create("https://control.example:8443"), recovered.route().endpoint());
assertFalse(recovered.hostedConfiguration().enabled());
@@ -100,7 +97,7 @@ class BackendControlResultStoreTest {
Files.writeString(external, "{}");
Files.createSymbolicLink(directory.resolve(".control-pending-results.json"), external);
assertThrows(java.io.IOException.class, () -> BackendControlResultStoreAccess.load(directory));
assertThrows(java.io.IOException.class, () -> BackendControlResultStore.load(directory));
}
@Test void maximumEscapeHeavyConfigurationFitsTheEncodedJournal() throws Exception {
@@ -114,12 +111,12 @@ class BackendControlResultStoreTest {
result.addProperty("success", true);
result.add("configuration", configuration);
BackendControlResultStoreAccess.save(directory, route, hosted,
BackendControlResultStore.save(directory, route, hosted,
Map.of(UUID.fromString("00000000-0000-0000-0000-000000000099"),
new StoredResult(result, false, true, false)));
assertEquals(BackendConfigurationService.MAX_CONTENT_BYTES,
BackendControlResultStoreAccess.load(directory).results().values().iterator().next().result()
BackendControlResultStore.load(directory).results().values().iterator().next().result()
.getAsJsonObject("configuration").get("content").getAsString().length());
}
}
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.control;
import com.bencodez.votingplugin.control.ControlInspectionService;
package com.bencodez.votingplugin.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -53,7 +51,7 @@ class ControlInspectionServiceTest {
when(plugin.getStorageType()).thenReturn(UserStorage.SQLITE);
when(plugin.getBackendProxyHandler()).thenReturn(null);
when(plugin.getServer().getPluginManager().getPlugins()).thenReturn(new org.bukkit.plugin.Plugin[0]);
JsonObject result = ControlInspectionServiceAccess.create(plugin).inspect(JsonParser.parseString("{\"kind\":\"network-health\",\"filters\":{}}").getAsJsonObject()).getAsJsonObject("result");
JsonObject result = new ControlInspectionService(plugin).inspect(JsonParser.parseString("{\"kind\":\"network-health\",\"filters\":{}}").getAsJsonObject()).getAsJsonObject("result");
assertEquals("REQUIRED", result.get("sharedAuthentication").getAsString());
String fixtureDir = System.getProperty("network.health.fixture.dir");
if (fixtureDir != null) {
@@ -71,13 +69,13 @@ class ControlInspectionServiceTest {
VotingPluginMain plugin = mock(VotingPluginMain.class, RETURNS_DEEP_STUBS);
when(plugin.getDataFolder()).thenReturn(directory.toFile());
JsonObject result = ControlInspectionServiceAccess.create(plugin).inspect(JsonParser.parseString(
JsonObject result = new ControlInspectionService(plugin).inspect(JsonParser.parseString(
"{\"kind\":\"reward-file-inventory\",\"filters\":{}}")
.getAsJsonObject()).getAsJsonObject("result");
assertEquals(List.of("Daily.yml"), result.getAsJsonArray("files").asList().stream()
.map(element -> element.getAsString()).toList());
assertTrue(ControlInspectionServiceAccess.rewardFileInventoryQuery(JsonParser.parseString(
assertTrue(ControlInspectionService.rewardFileInventoryQuery(JsonParser.parseString(
"{\"kind\":\"reward-file-inventory\"}").getAsJsonObject()));
}
@@ -92,21 +90,21 @@ class ControlInspectionServiceTest {
.mapToObj(index -> mock(org.bukkit.plugin.Plugin.class, RETURNS_DEEP_STUBS)).toArray(org.bukkit.plugin.Plugin[]::new));
org.bukkit.plugin.Plugin[] plugins = plugin.getServer().getPluginManager().getPlugins();
for (int index = 0; index < plugins.length; index++) when(plugins[index].getName()).thenReturn("Plugin" + index);
JsonObject result = ControlInspectionServiceAccess.create(plugin).inspect(JsonParser.parseString(
JsonObject result = new ControlInspectionService(plugin).inspect(JsonParser.parseString(
"{\"kind\":\"network-health\",\"filters\":{}}").getAsJsonObject()).getAsJsonObject("result");
assertFalse(result.has("detectedPlugins"));
org.bukkit.plugin.Plugin[] complete = new org.bukkit.plugin.Plugin[ControlInspectionServiceAccess.MAX_ROWS];
org.bukkit.plugin.Plugin[] complete = new org.bukkit.plugin.Plugin[ControlInspectionService.MAX_ROWS];
for (int index = 0; index < complete.length; index++) {
complete[index] = mock(org.bukkit.plugin.Plugin.class, RETURNS_DEEP_STUBS);
when(complete[index].getName()).thenReturn("Complete" + index);
}
when(plugin.getServer().getPluginManager().getPlugins()).thenReturn(complete);
result = ControlInspectionServiceAccess.create(plugin).inspect(JsonParser.parseString(
result = new ControlInspectionService(plugin).inspect(JsonParser.parseString(
"{\"kind\":\"network-health\",\"filters\":{}}").getAsJsonObject()).getAsJsonObject("result");
assertEquals(ControlInspectionServiceAccess.MAX_ROWS, result.getAsJsonArray("detectedPlugins").size());
complete[ControlInspectionServiceAccess.MAX_ROWS - 1] = mock(org.bukkit.plugin.Plugin.class, RETURNS_DEEP_STUBS);
when(complete[ControlInspectionServiceAccess.MAX_ROWS - 1].getName()).thenReturn("bad\nname");
result = ControlInspectionServiceAccess.create(plugin).inspect(JsonParser.parseString(
assertEquals(ControlInspectionService.MAX_ROWS, result.getAsJsonArray("detectedPlugins").size());
complete[ControlInspectionService.MAX_ROWS - 1] = mock(org.bukkit.plugin.Plugin.class, RETURNS_DEEP_STUBS);
when(complete[ControlInspectionService.MAX_ROWS - 1].getName()).thenReturn("bad\nname");
result = new ControlInspectionService(plugin).inspect(JsonParser.parseString(
"{\"kind\":\"network-health\",\"filters\":{}}").getAsJsonObject()).getAsJsonObject("result");
assertFalse(result.has("detectedPlugins"));
}
@@ -119,7 +117,7 @@ class ControlInspectionServiceTest {
when(plugin.getConfigVoteSites().getData()).thenReturn(voteSites);
when(plugin.getVoteSiteManager().getVoteSites()).thenReturn(new ArrayList<>());
when(plugin.getVoteLogMysqlTable()).thenReturn(null);
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"overview\",\"filters\":{}}")
@@ -130,7 +128,7 @@ class ControlInspectionServiceTest {
}
@Test void rewardSimulationNormalizesButNeverExecutesTypedActions() {
ControlInspectionService service = ControlInspectionServiceAccess.create();
ControlInspectionService service = new ControlInspectionService();
JsonObject proposal = JsonParser.parseString("""
{"scope":"site","site":"PMC","commands":["eco give %player% 100"],
"playerMessages":["Thanks"],"items":[{"material":"diamond","amount":2}],
@@ -155,7 +153,7 @@ class ControlInspectionServiceTest {
}
@Test void inspectionContractRejectsUnknownKindsFieldsAndUnboundedSearches() {
ControlInspectionService service = ControlInspectionServiceAccess.create();
ControlInspectionService service = new ControlInspectionService();
assertThrows(IllegalArgumentException.class,
() -> service.inspect(JsonParser.parseString("{\"kind\":\"raw-sql\"}").getAsJsonObject()));
@@ -194,7 +192,7 @@ class ControlInspectionServiceTest {
}
@Test void rewardProposalRejectsUnknownActionsAndEmptyPlans() {
ControlInspectionService service = ControlInspectionServiceAccess.create();
ControlInspectionService service = new ControlInspectionService();
assertThrows(IllegalArgumentException.class, () -> service.inspect(rewardQuery(JsonParser.parseString(
"{\"scope\":\"site\",\"site\":\"PMC\",\"shell\":\"rm\"}").getAsJsonObject())));
assertThrows(IllegalArgumentException.class, () -> service.inspect(rewardQuery(JsonParser.parseString(
@@ -211,7 +209,7 @@ class ControlInspectionServiceTest {
VotingPluginMain plugin = mock(VotingPluginMain.class, RETURNS_DEEP_STUBS);
when(plugin.getConfigVoteSites().getData())
.thenReturn(new org.bukkit.configuration.file.YamlConfiguration());
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
assertThrows(IllegalArgumentException.class, () -> service.inspect(rewardQuery(JsonParser.parseString(
"{\"scope\":\"site\",\"site\":\"Missing\",\"commands\":[\"say test\"]}"
@@ -222,7 +220,7 @@ class ControlInspectionServiceTest {
VotingPluginMain plugin = mock(VotingPluginMain.class, RETURNS_DEEP_STUBS);
VoteLogMysqlTable staleTable = mock(VoteLogMysqlTable.class);
when(plugin.getVoteLogMysqlTable()).thenReturn(staleTable);
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject query = JsonParser.parseString(
"{\"kind\":\"vote-log-summary\",\"filters\":{\"days\":\"30\"}}")
.getAsJsonObject();
@@ -238,7 +236,7 @@ class ControlInspectionServiceTest {
when(plugin.getConfigFile().isVoteLoggingEnabled()).thenReturn(true);
when(plugin.getVoteLogMysqlTable()).thenReturn(table);
when(table.isReadable()).thenReturn(false);
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject query = JsonParser.parseString(
"{\"kind\":\"vote-log-summary\",\"filters\":{\"days\":\"30\"}}")
.getAsJsonObject();
@@ -260,7 +258,7 @@ class ControlInspectionServiceTest {
new ServiceCount("Zulu", 3), new ServiceCount("alpha", 3), new ServiceCount("Middle", 6)));
when(table.getTopServers(30, 20)).thenReturn(List.of(
new ServerCount("survival", 2), new ServerCount("Creative", 2), new ServerCount("hub", 8)));
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"vote-log-summary\",\"filters\":{\"days\":\"30\"}}")
@@ -283,7 +281,7 @@ class ControlInspectionServiceTest {
@Test void exactPlayerMissDoesNotLoadOrEnumerateUsers() {
VotingPluginMain plugin = mock(VotingPluginMain.class, RETURNS_DEEP_STUBS);
when(plugin.getUserManager().userExist("ExactName")).thenReturn(false);
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject query = JsonParser.parseString(
"{\"kind\":\"player\",\"filters\":{\"name\":\"ExactName\"}}")
.getAsJsonObject();
@@ -308,7 +306,7 @@ class ControlInspectionServiceTest {
lastVotes.put(earlier, 100L);
when(user.getLastVotes()).thenReturn(lastVotes);
when(user.getLastVoteTime()).thenReturn(200L);
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"player\",\"filters\":{\"name\":\"ExactName\"}}")
@@ -350,7 +348,7 @@ class ControlInspectionServiceTest {
stored.put("MonthTotal-SMARCH-2025", new DataValueInt(95));
stored.put("MonthTotal-FEBRUARY-2025", new DataValueString("wrong type"));
when(user.getUserData().getValues()).thenReturn(stored);
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"player\",\"filters\":{\"name\":\"ExactName\"}}")
@@ -386,20 +384,20 @@ class ControlInspectionServiceTest {
when(user.getLastVotes()).thenReturn(new HashMap<>());
HashMap<String, DataValue> stored = new HashMap<>();
List<String> expected = new ArrayList<>();
for (int index = 0; index <= ControlInspectionServiceAccess.MAX_ROWS; index++) {
for (int index = 0; index <= ControlInspectionService.MAX_ROWS; index++) {
String name = "MonthTotal-" + java.time.Month.of(index % 12 + 1).name() + "-" + (2000 + index / 12);
stored.put(name, new DataValueInt(index));
expected.add(name);
}
expected.sort(String.CASE_INSENSITIVE_ORDER.thenComparing(java.util.Comparator.naturalOrder()));
when(user.getUserData().getValues()).thenReturn(stored);
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"player\",\"filters\":{\"name\":\"ExactName\"}}")
.getAsJsonObject()).getAsJsonObject("result");
assertEquals(expected.subList(0, ControlInspectionServiceAccess.MAX_ROWS),
assertEquals(expected.subList(0, ControlInspectionService.MAX_ROWS),
result.getAsJsonArray("columns").asList().stream()
.map(value -> value.getAsJsonObject().get("name").getAsString()).toList());
assertTrue(result.get("columnsTruncated").getAsBoolean());
@@ -416,17 +414,17 @@ class ControlInspectionServiceTest {
when(user.getOfflineVotes()).thenReturn(new ArrayList<>());
when(user.getLastVotes()).thenReturn(new HashMap<>());
HashMap<String, DataValue> stored = new HashMap<>();
for (int index = 0; index <= ControlInspectionServiceAccess.MAX_ROWS; index++) {
for (int index = 0; index <= ControlInspectionService.MAX_ROWS; index++) {
stored.put(String.format("VoteShopLimit%03d", index), new DataValueInt(index));
}
when(user.getUserData().getValues()).thenReturn(stored);
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"player\",\"filters\":{\"name\":\"ExactName\"}}")
.getAsJsonObject()).getAsJsonObject("result");
assertEquals(ControlInspectionServiceAccess.MAX_ROWS, result.getAsJsonArray("columns").size());
assertEquals(ControlInspectionService.MAX_ROWS, result.getAsJsonArray("columns").size());
assertEquals("VoteShopLimit000", result.getAsJsonArray("columns").get(0).getAsJsonObject()
.get("name").getAsString());
assertEquals("VoteShopLimit099", result.getAsJsonArray("columns").get(99).getAsJsonObject()
@@ -442,7 +440,7 @@ class ControlInspectionServiceTest {
when(plugin.getConfigVoteSites().getData()).thenReturn(voteSites);
when(plugin.getVoteSiteManager().getVoteSites()).thenReturn(new ArrayList<>());
when(plugin.getVoteLogMysqlTable()).thenReturn(null);
org.bukkit.plugin.Plugin[] installed = new org.bukkit.plugin.Plugin[ControlInspectionServiceAccess.MAX_ROWS + 1];
org.bukkit.plugin.Plugin[] installed = new org.bukkit.plugin.Plugin[ControlInspectionService.MAX_ROWS + 1];
for (int index = 0; index < installed.length; index++) {
installed[index] = mock(org.bukkit.plugin.Plugin.class, RETURNS_DEEP_STUBS);
when(installed[index].getDescription().getName()).thenReturn(String.format("Plugin%03d", index));
@@ -450,13 +448,13 @@ class ControlInspectionServiceTest {
when(installed[99].getDescription().getName()).thenReturn("plugina");
when(installed[100].getDescription().getName()).thenReturn("PluginA");
when(plugin.getServer().getPluginManager().getPlugins()).thenReturn(installed);
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"diagnostics\",\"filters\":{}}")
.getAsJsonObject()).getAsJsonObject("result");
assertEquals(ControlInspectionServiceAccess.MAX_ROWS, result.getAsJsonArray("detectedPlugins").size());
assertEquals(ControlInspectionService.MAX_ROWS, result.getAsJsonArray("detectedPlugins").size());
assertEquals("Plugin000", result.getAsJsonArray("detectedPlugins").get(0).getAsString());
assertEquals("PluginA", result.getAsJsonArray("detectedPlugins").get(99).getAsString());
assertTrue(result.get("detectedPluginsTruncated").getAsBoolean());
@@ -470,7 +468,7 @@ class ControlInspectionServiceTest {
when(plugin.getVoteLogMysqlTable()).thenReturn(null);
when(plugin.getServerData().getServiceSitesReadOnly()).thenReturn(java.util.List.of(
"NEW.example", "new.EXAMPLE", "configured.example"));
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"vote-site-health\",\"filters\":{\"days\":\"30\"}}")
@@ -495,7 +493,7 @@ class ControlInspectionServiceTest {
when(plugin.getVoteLogMysqlTable()).thenReturn(table);
when(table.isReadable()).thenReturn(false);
when(plugin.getServerData().getServiceSitesReadOnly()).thenReturn(java.util.List.of());
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"vote-site-health\",\"filters\":{\"days\":\"30\"}}")
@@ -525,7 +523,7 @@ class ControlInspectionServiceTest {
when(table.getServiceHealthForServices(30, List.of("configured.example"))).thenReturn(List.of(
new ServiceHealth("configured.example", 7, 500, 6, 1)));
when(plugin.getServerData().getServiceSitesReadOnly()).thenReturn(List.of());
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"vote-site-health\",\"filters\":{\"days\":\"30\"}}")
@@ -553,7 +551,7 @@ class ControlInspectionServiceTest {
new ServiceHealth(fullService.toUpperCase(Locale.ROOT), 5, 900, 4, 1)));
when(plugin.getServerData().getServiceSitesReadOnly()).thenReturn(List.of(
fullService.toUpperCase(Locale.ROOT)));
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"vote-site-health\",\"filters\":{\"days\":\"30\"}}")
@@ -585,7 +583,7 @@ class ControlInspectionServiceTest {
new ServiceHealth("configured-100.example", 3, 800, 3, 0)));
when(table.getServiceHealthForServices(30, displayed)).thenReturn(List.of());
when(plugin.getServerData().getServiceSitesReadOnly()).thenReturn(List.of());
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"vote-site-health\",\"filters\":{\"days\":\"30\"}}")
@@ -610,7 +608,7 @@ class ControlInspectionServiceTest {
new ServiceHealth("example.COM", 4, 900, 3, 1)));
when(table.getServiceHealthForServices(30, List.of("example.com"))).thenReturn(List.of());
when(plugin.getServerData().getServiceSitesReadOnly()).thenReturn(List.of());
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"vote-site-health\",\"filters\":{\"days\":\"30\"}}")
@@ -631,7 +629,7 @@ class ControlInspectionServiceTest {
.thenReturn(null);
when(plugin.getVoteSiteManager().resolveVoteSite("new.example", true)).thenReturn(null);
when(plugin.getConfigFile().isAutoCreateVoteSites()).thenReturn(true);
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"vote-site-resolution\",\"filters\":{\"serviceSite\":\"new.example\"}}")
@@ -656,7 +654,7 @@ class ControlInspectionServiceTest {
when(plugin.getVoteSiteManager().resolveVoteSite("PMC", true)).thenReturn(site);
when(plugin.getVoteSiteManager().hasVoteSite("PMC")).thenReturn(true);
when(plugin.getConfigFile().isAutoCreateVoteSites()).thenReturn(true);
ControlInspectionService service = ControlInspectionServiceAccess.create(plugin);
ControlInspectionService service = new ControlInspectionService(plugin);
JsonObject result = service.inspect(JsonParser.parseString(
"{\"kind\":\"vote-site-resolution\",\"filters\":{\"serviceSite\":\"planetminecraft.com\"}}")
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.control;
import com.bencodez.votingplugin.tests.control.NetworkHealthConfigFactsAccess;
package com.bencodez.votingplugin.control;
import static org.junit.jupiter.api.Assertions.*;
@@ -17,7 +15,7 @@ class NetworkHealthConfigFactsTest {
yaml.set("Database.Password", "secret");
yaml.set("BedrockPlayerPrefix", "x".repeat(81));
JsonObject out = new JsonObject();
NetworkHealthConfigFactsAccess.addBackend(out, yaml);
NetworkHealthConfigFacts.addBackend(out, yaml);
assertTrue(out.get("onlineMode").getAsBoolean());
assertFalse(out.has("maxVotesPerDay"));
assertFalse(out.has("Database.Password"));
@@ -34,8 +32,8 @@ class NetworkHealthConfigFactsTest {
special.set("VoteParty.VotesRequired", 12);
special.set("ResetMilestonesMonthly", false);
JsonObject out = new JsonObject();
NetworkHealthConfigFactsAccess.addBackend(out, main);
NetworkHealthConfigFactsAccess.addSpecialRewards(out, special);
NetworkHealthConfigFacts.addBackend(out, main);
NetworkHealthConfigFacts.addSpecialRewards(out, special);
assertFalse(out.get("votePartyEnabled").getAsBoolean());
assertEquals(12, out.get("votePartyVotesRequired").getAsInt());
assertFalse(out.get("resetMilestonesMonthly").getAsBoolean());
@@ -46,7 +44,7 @@ class NetworkHealthConfigFactsTest {
yaml.set("GlobalData.Prefix", "p".repeat(81));
yaml.set("GlobalData.UseMainMySQL", "yes");
JsonObject out = new JsonObject();
NetworkHealthConfigFactsAccess.addGlobal(out, yaml);
NetworkHealthConfigFacts.addGlobal(out, yaml);
assertFalse(out.has("globalDataPrefix"));
assertFalse(out.has("globalDataUseMainMysql"));
}
@@ -55,10 +53,10 @@ class NetworkHealthConfigFactsTest {
YamlConfiguration yaml = new YamlConfiguration();
yaml.set("TimeHourOffSet", Integer.MAX_VALUE);
JsonObject out = new JsonObject();
NetworkHealthConfigFactsAccess.addBackend(out, yaml);
NetworkHealthConfigFacts.addBackend(out, yaml);
assertFalse(out.has("timeHourOffset"));
yaml.set("TimeHourOffSet", -3);
NetworkHealthConfigFactsAccess.addBackend(out, yaml);
NetworkHealthConfigFacts.addBackend(out, yaml);
assertEquals(-3, out.get("timeHourOffset").getAsInt());
}
@@ -68,7 +66,7 @@ class NetworkHealthConfigFactsTest {
special.set("VoteParty.VotesRequired", "many");
special.set("ResetMilestonesMonthly", 1);
JsonObject out = new JsonObject();
NetworkHealthConfigFactsAccess.addSpecialRewards(out, special);
NetworkHealthConfigFacts.addSpecialRewards(out, special);
assertFalse(out.has("votePartyEnabled"));
assertFalse(out.has("votePartyVotesRequired"));
assertFalse(out.has("resetMilestonesMonthly"));
@@ -80,31 +78,31 @@ class NetworkHealthConfigFactsTest {
YamlConfiguration yaml = new YamlConfiguration();
yaml.set("TimeHourOffSet", Long.MIN_VALUE);
JsonObject out = new JsonObject();
NetworkHealthConfigFactsAccess.addBackend(out, yaml);
NetworkHealthConfigFacts.addBackend(out, yaml);
assertFalse(out.has("timeHourOffset"));
}
@Test void delayParserEmptyResultCannotProveMalformedTextValid() {
for (String text : java.util.List.of("abc", "-5h", "")) {
JsonObject out = new JsonObject();
NetworkHealthConfigFactsAccess.addDelay(out, text, null, com.bencodez.simpleapi.time.ParsedDuration.parse(text, java.util.concurrent.TimeUnit.HOURS));
NetworkHealthConfigFacts.addDelay(out, text, null, com.bencodez.simpleapi.time.ParsedDuration.parse(text, java.util.concurrent.TimeUnit.HOURS));
assertFalse(out.get("delayValid").getAsBoolean());
}
JsonObject halfHour = new JsonObject();
NetworkHealthConfigFactsAccess.addDelay(halfHour, "30m", null, com.bencodez.simpleapi.time.ParsedDuration.parse("30m"));
NetworkHealthConfigFacts.addDelay(halfHour, "30m", null, com.bencodez.simpleapi.time.ParsedDuration.parse("30m"));
assertTrue(halfHour.get("delayValid").getAsBoolean()); assertEquals(1, halfHour.get("delayHours").getAsInt());
JsonObject zero = new JsonObject();
NetworkHealthConfigFactsAccess.addDelay(zero, "0", null, com.bencodez.simpleapi.time.ParsedDuration.empty());
NetworkHealthConfigFacts.addDelay(zero, "0", null, com.bencodez.simpleapi.time.ParsedDuration.empty());
assertTrue(zero.get("delayValid").getAsBoolean()); assertEquals(0, zero.get("delayHours").getAsInt());
}
@Test void incompleteServiceObservationsAreOmittedInsteadOfEmpty() {
JsonObject invalid = new JsonObject();
NetworkHealthConfigFactsAccess.addObservedServices(invalid, java.util.List.of("unmatched", "s".repeat(81)));
NetworkHealthConfigFacts.addObservedServices(invalid, java.util.List.of("unmatched", "s".repeat(81)));
assertFalse(invalid.has("detectedServices"));
JsonObject large = new JsonObject();
NetworkHealthConfigFactsAccess.addObservedServices(large, java.util.stream.IntStream.range(0, 101).mapToObj(i -> "site" + i).toList());
NetworkHealthConfigFacts.addObservedServices(large, java.util.stream.IntStream.range(0, 101).mapToObj(i -> "site" + i).toList());
assertFalse(large.has("detectedServices"));
JsonObject valid = new JsonObject();
NetworkHealthConfigFactsAccess.addObservedServices(valid, java.util.List.of("site", "SITE", "other"));
NetworkHealthConfigFacts.addObservedServices(valid, java.util.List.of("site", "SITE", "other"));
assertEquals(2, valid.getAsJsonArray("detectedServices").size());
}
@@ -1,8 +1,4 @@
package com.bencodez.votingplugin.tests.control;
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
import com.bencodez.votingplugin.control.OptionalVotifierDiagnostics;
package com.bencodez.votingplugin.control;
import com.google.gson.JsonObject;
import java.lang.reflect.*;
@@ -51,7 +47,7 @@ class NuVotifierJarDiagnosticsTest {
String packageName = platform.substring(0, platform.indexOf('.'));
Object pm = mock(Class.forName("com.vexsoftware.votifier." + packageName + "." + sourceName, false, loader));
Class<?> base = pm.getClass().getSuperclass();
Field owner = base.getDeclaredField("plugin"); owner.setAccessible(true); ReflectiveAccess.set(owner, pm, provider);
Field owner = base.getDeclaredField("plugin"); owner.setAccessible(true); owner.set(pm, provider);
Field filter = base.getDeclaredField("serverFilter"); filter.setAccessible(true);
Class<?> backendApi = Class.forName("com.vexsoftware.votifier.platform.BackendServer", false, loader);
Object backend = mock(backendApi);
@@ -61,31 +57,31 @@ class NuVotifierJarDiagnosticsTest {
Constructor<?> filterCtor = Class.forName("com.vexsoftware.votifier.support.forwarding.ServerFilter", false, loader)
.getConstructor(Collection.class, boolean.class);
setSource(provider, pm);
ReflectiveAccess.set(filter, pm, ReflectiveAccess.newInstance(filterCtor, new Object[]{List.of("backend-a"), true}));
filter.set(pm, filterCtor.newInstance(List.of("backend-a"), true));
assertTrue(facts(provider).get("votifierForwardingEnabled").getAsBoolean());
assertEquals("backend-a", facts(provider).getAsJsonArray("forwardingDestinations").get(0).getAsString());
ReflectiveAccess.set(filter, pm, ReflectiveAccess.newInstance(filterCtor, new Object[]{List.of(), true}));
filter.set(pm, filterCtor.newInstance(List.of(), true));
assertFalse(facts(provider).get("votifierForwardingEnabled").getAsBoolean());
ReflectiveAccess.set(filter, pm, ReflectiveAccess.newInstance(filterCtor, new Object[]{List.of("backend-a"), false}));
filter.set(pm, filterCtor.newInstance(List.of("backend-a"), false));
assertFalse(facts(provider).get("votifierForwardingEnabled").getAsBoolean());
if (sourceName.startsWith("OnlineForward")) {
Field fallback = pm.getClass().getDeclaredField("fallbackServer"); fallback.setAccessible(true);
when(proxyApi.getMethod("getServer", String.class).invoke(provider, "backend-a"))
.thenReturn(Optional.of(backend));
ReflectiveAccess.set(fallback, pm, "backend-a");
fallback.set(pm, "backend-a");
// Both an empty whitelist and a rejecting blacklist still allow the fallback.
for (boolean whitelist : List.of(true, false)) {
ReflectiveAccess.set(filter, pm, ReflectiveAccess.newInstance(filterCtor, new Object[]{whitelist ? List.of() : List.of("backend-a"), whitelist}));
filter.set(pm, filterCtor.newInstance(whitelist ? List.of() : List.of("backend-a"), whitelist));
assertTrue(facts(provider).get("votifierForwardingEnabled").getAsBoolean());
assertEquals("backend-a", facts(provider).getAsJsonArray("forwardingDestinations").get(0).getAsString());
}
when(proxyApi.getMethod("getServer", String.class).invoke(provider, "backend-a"))
.thenReturn(Optional.empty());
assertFalse(facts(provider).get("votifierForwardingEnabled").getAsBoolean());
ReflectiveAccess.set(fallback, pm, "bad\nname"); assertUnknown(provider);
ReflectiveAccess.set(fallback, pm, null);
fallback.set(pm, "bad\nname"); assertUnknown(provider);
fallback.set(pm, null);
}
ReflectiveAccess.set(filter, pm, null); assertUnknown(provider);
filter.set(pm, null); assertUnknown(provider);
}
}
}
@@ -96,12 +92,13 @@ class NuVotifierJarDiagnosticsTest {
Constructor<?> backend = Class.forName(source.getName() + "$BackendServer", false, loader)
.getConstructor(String.class, InetSocketAddress.class, java.security.Key.class);
List<Object> targets = new ArrayList<>();
for (String name : names) targets.add(ReflectiveAccess.newInstance(backend, new Object[]{name, InetSocketAddress.createUnresolved("not-exported.invalid", 8192), null}));
for (String name : names) targets.add(backend.newInstance(name,
InetSocketAddress.createUnresolved("not-exported.invalid", 8192), null));
Constructor<?> ctor = Arrays.stream(source.getConstructors()).filter(c -> c.getParameterCount() == 4).findFirst().orElseThrow();
return ReflectiveAccess.newInstance(ctor, new Object[]{null, null, targets, null}); // stores fields; no start/forward call
return ctor.newInstance(null, null, targets, null); // stores fields; no start/forward call
}
private static void setSource(Object provider, Object source) throws Exception {
Field field = provider.getClass().getDeclaredField("forwardingMethod"); field.setAccessible(true); ReflectiveAccess.set(field, provider, source);
Field field = provider.getClass().getDeclaredField("forwardingMethod"); field.setAccessible(true); field.set(provider, source);
}
private static JsonObject facts(Object provider) { return facts(List.of(provider)); }
private static JsonObject facts(List<?> providers) {
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.control;
import com.bencodez.votingplugin.control.OptionalVotifierDiagnostics;
package com.bencodez.votingplugin.control;
import com.google.gson.JsonObject;
import java.util.*;
import org.junit.jupiter.api.Test;
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.control;
import com.bencodez.votingplugin.control.PluginDeploymentService;
package com.bencodez.votingplugin.control;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@@ -45,7 +43,7 @@ class PluginDeploymentServiceTest {
java.util.Map.of("create", "true"))) {
Path destination = zip.getPath("/VotingPlugin.jar");
assertThrows(AtomicMoveNotSupportedException.class,
() -> PluginDeploymentServiceAccess.move(source, destination));
() -> PluginDeploymentService.move(source, destination));
assertArrayEquals(original, Files.readAllBytes(source));
assertFalse(Files.exists(destination));
}
@@ -56,12 +54,12 @@ class PluginDeploymentServiceTest {
PluginDeploymentService service = PluginDeploymentService.backend(directory.resolve("update"), Path.of("VotingPlugin.jar"));
PluginDeploymentService.Task task = task(artifact);
PluginDeploymentService.Result staged = PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(artifact), () -> true);
PluginDeploymentService.Result staged = service.stage(task, new ByteArrayInputStream(artifact), () -> true);
assertTrue(staged.success());
assertEquals("RESTART_REQUIRED", staged.code());
assertEquals(artifact.length, Files.size(directory.resolve("update/VotingPlugin.jar")));
assertTrue(PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(new byte[0]), () -> true).success());
assertTrue(service.stage(task, new ByteArrayInputStream(new byte[0]), () -> true).success());
}
@Test void backendStagesUsingTheInstalledPluginJarFileName() throws Exception {
@@ -70,7 +68,7 @@ class PluginDeploymentServiceTest {
PluginDeploymentService service = PluginDeploymentService.backend(update,
directory.resolve("plugins/VotingPlugin-7.1.2.jar"));
assertEquals("RESTART_REQUIRED", PluginDeploymentServiceAccess.stage(service, task(artifact), new ByteArrayInputStream(artifact), () -> true).code());
assertEquals("RESTART_REQUIRED", service.stage(task(artifact), new ByteArrayInputStream(artifact), () -> true).code());
assertArrayEquals(artifact, Files.readAllBytes(update.resolve("VotingPlugin-7.1.2.jar")));
assertFalse(Files.exists(update.resolve("VotingPlugin.jar")));
}
@@ -88,11 +86,11 @@ class PluginDeploymentServiceTest {
PluginDeploymentService service = PluginDeploymentService.backend(update, installed);
PluginDeploymentService.Task task = task(artifact);
assertTrue(PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(artifact), () -> true).success());
assertTrue(service.stage(task, new ByteArrayInputStream(artifact), () -> true).success());
Files.write(installed, artifact);
Files.delete(update.resolve("VotingPlugin.jar"));
assertTrue(PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(new byte[0]), () -> true).success());
assertTrue(service.stage(task, new ByteArrayInputStream(new byte[0]), () -> true).success());
assertFalse(Files.exists(update.resolve("VotingPlugin.jar")),
"a lost acknowledgement after restart must recognize the artifact Bukkit already consumed");
}
@@ -105,17 +103,17 @@ class PluginDeploymentServiceTest {
PluginDeploymentService service = PluginDeploymentService.backend(update, installed);
PluginDeploymentService.Task original = task(artifact);
PluginDeploymentService.Task retry = task(artifact);
assertTrue(PluginDeploymentServiceAccess.stage(service, original, new ByteArrayInputStream(artifact), () -> true).success());
assertTrue(service.stage(original, new ByteArrayInputStream(artifact), () -> true).success());
assertFalse(original.deploymentId().equals(retry.deploymentId()));
assertEquals("RESTART_REQUIRED",
PluginDeploymentServiceAccess.stage(service, retry, new ByteArrayInputStream(new byte[0]), () -> true).code());
service.stage(retry, new ByteArrayInputStream(new byte[0]), () -> true).code());
assertArrayEquals(artifact, Files.readAllBytes(update.resolve("VotingPlugin.jar")));
Files.write(installed, artifact);
Files.delete(update.resolve("VotingPlugin.jar"));
assertEquals("RESTART_REQUIRED",
PluginDeploymentServiceAccess.stage(service, retry, new ByteArrayInputStream(new byte[0]), () -> true).code());
service.stage(retry, new ByteArrayInputStream(new byte[0]), () -> true).code());
assertFalse(Files.exists(update.resolve("VotingPlugin.jar")));
}
@@ -127,10 +125,10 @@ class PluginDeploymentServiceTest {
PluginDeploymentService service = PluginDeploymentService.backend(update, installed);
PluginDeploymentService.Task task = task(artifact);
assertTrue(PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(artifact), () -> true).success());
assertTrue(service.stage(task, new ByteArrayInputStream(artifact), () -> true).success());
Files.delete(update.resolve("VotingPlugin.jar"));
assertTrue(PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(artifact), () -> true).success());
assertTrue(service.stage(task, new ByteArrayInputStream(artifact), () -> true).success());
assertArrayEquals(artifact, Files.readAllBytes(update.resolve("VotingPlugin.jar")),
"a deleted or quarantined update must be staged again before restart");
}
@@ -203,7 +201,7 @@ class PluginDeploymentServiceTest {
PluginDeploymentService service = PluginDeploymentService.backend(directory.resolve("update"), Path.of("VotingPlugin.jar"));
PluginDeploymentService.Task task = task(artifact);
PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(artifact), () -> true);
service.stage(task, new ByteArrayInputStream(artifact), () -> true);
assertArrayEquals(artifact, Files.readAllBytes(directory.resolve("update/VotingPlugin.jar")));
byte[] corrupted = new byte[artifact.length];
@@ -211,14 +209,14 @@ class PluginDeploymentServiceTest {
Files.write(directory.resolve("update/VotingPlugin.jar"), corrupted);
assertArrayEquals(corrupted, Files.readAllBytes(directory.resolve("update/VotingPlugin.jar")));
assertTrue(PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(artifact), () -> true).success());
assertTrue(service.stage(task, new ByteArrayInputStream(artifact), () -> true).success());
assertArrayEquals(artifact, Files.readAllBytes(directory.resolve("update/VotingPlugin.jar")));
}
@Test void invalidPluginIdentityAndDigestNeverReachTheUpdateFolder() throws Exception {
byte[] wrongPlugin = jar("name: NotVotingPlugin\n");
PluginDeploymentService service = PluginDeploymentService.backend(directory.resolve("update"), Path.of("VotingPlugin.jar"));
PluginDeploymentService.Result invalid = PluginDeploymentServiceAccess.stage(service, task(wrongPlugin), new ByteArrayInputStream(wrongPlugin), () -> true);
PluginDeploymentService.Result invalid = service.stage(task(wrongPlugin), new ByteArrayInputStream(wrongPlugin), () -> true);
assertFalse(invalid.success());
assertEquals("INVALID_ARTIFACT", invalid.code());
assertFalse(Files.exists(directory.resolve("update/VotingPlugin.jar")));
@@ -226,7 +224,7 @@ class PluginDeploymentServiceTest {
byte[] good = jar("name: VotingPlugin\n");
PluginDeploymentService.Task mismatch = new PluginDeploymentService.Task(UUID.randomUUID(), "vp.jar",
"0".repeat(64), good.length, UUID.randomUUID());
assertEquals("HASH_MISMATCH", PluginDeploymentServiceAccess.stage(service, mismatch, new ByteArrayInputStream(good), () -> true).code());
assertEquals("HASH_MISMATCH", service.stage(mismatch, new ByteArrayInputStream(good), () -> true).code());
}
@Test void proxyKeepsTheCurrentJarAsABackupBeforeReplacement() throws Exception {
@@ -237,8 +235,8 @@ class PluginDeploymentServiceTest {
Files.write(current, original);
PluginDeploymentService service = PluginDeploymentService.proxy(current);
assertEquals("RESTART_REQUIRED", PluginDeploymentServiceAccess.stage(service, task(first), new ByteArrayInputStream(first), () -> true).code());
assertEquals("RESTART_REQUIRED", PluginDeploymentServiceAccess.stage(service, task(second), new ByteArrayInputStream(second), () -> true).code());
assertEquals("RESTART_REQUIRED", service.stage(task(first), new ByteArrayInputStream(first), () -> true).code());
assertEquals("RESTART_REQUIRED", service.stage(task(second), new ByteArrayInputStream(second), () -> true).code());
assertArrayEquals(first, Files.readAllBytes(directory.resolve("VotingPlugin.jar.control-backup")));
assertArrayEquals(second, Files.readAllBytes(current));
}
@@ -255,11 +253,11 @@ class PluginDeploymentServiceTest {
PluginDeploymentService service = PluginDeploymentService.proxy(current);
assertEquals("SIZE_MISMATCH",
PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(new byte[0]), () -> true).code());
service.stage(task, new ByteArrayInputStream(new byte[0]), () -> true).code());
assertFalse(Files.exists(marker));
assertArrayEquals(original, Files.readAllBytes(backup));
assertEquals("RESTART_REQUIRED",
PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(candidate), () -> true).code());
service.stage(task, new ByteArrayInputStream(candidate), () -> true).code());
assertArrayEquals(candidate, Files.readAllBytes(current));
assertArrayEquals(candidate, Files.readAllBytes(backup));
@@ -277,10 +275,10 @@ class PluginDeploymentServiceTest {
Files.write(current, original);
PluginDeploymentService service = PluginDeploymentService.proxy(current);
PluginDeploymentService.Task task = task(candidate);
assertEquals("RESTART_REQUIRED", PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(candidate), () -> true).code());
assertEquals("RESTART_REQUIRED", service.stage(task, new ByteArrayInputStream(candidate), () -> true).code());
Files.delete(backup);
assertEquals("SIZE_MISMATCH", PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(new byte[0]), () -> true).code());
assertEquals("RESTART_REQUIRED", PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(candidate), () -> true).code());
assertEquals("SIZE_MISMATCH", service.stage(task, new ByteArrayInputStream(new byte[0]), () -> true).code());
assertEquals("RESTART_REQUIRED", service.stage(task, new ByteArrayInputStream(candidate), () -> true).code());
assertTrue(Files.isRegularFile(backup));
}
@@ -289,7 +287,7 @@ class PluginDeploymentServiceTest {
PluginDeploymentService service = PluginDeploymentService.backend(directory.resolve("update"), Path.of("VotingPlugin.jar"));
PluginDeploymentService.Task task = task(artifact);
assertEquals("RESTART_REQUIRED", PluginDeploymentServiceAccess.stage(service, task, new ByteArrayInputStream(artifact), () -> true).code());
assertEquals("RESTART_REQUIRED", service.stage(task, new ByteArrayInputStream(artifact), () -> true).code());
String markerBefore = Files.readString(directory.resolve("update/VotingPlugin.jar.control-deployment"));
byte[] targetBefore = Files.readAllBytes(directory.resolve("update/VotingPlugin.jar"));
@@ -297,7 +295,7 @@ class PluginDeploymentServiceTest {
byte[] replacementArtifact = jar("name: VotingPlugin\nversion: replacement\n");
InputStream body = new CancellingInputStream(new ByteArrayInputStream(replacementArtifact), active);
PluginDeploymentService.Task replacement = task(replacementArtifact);
assertEquals("CANCELLED", PluginDeploymentServiceAccess.stage(service, replacement, body, active::get).code());
assertEquals("CANCELLED", service.stage(replacement, body, active::get).code());
assertEquals(markerBefore, Files.readString(directory.resolve("update/VotingPlugin.jar.control-deployment")));
assertArrayEquals(targetBefore, Files.readAllBytes(directory.resolve("update/VotingPlugin.jar")));
@@ -316,7 +314,7 @@ class PluginDeploymentServiceTest {
Files.writeString(marker.resolve("keep"), "marker publication must fail");
PluginDeploymentService service = PluginDeploymentService.backend(update, Path.of("VotingPlugin.jar"));
assertThrows(IOException.class, () -> PluginDeploymentServiceAccess.stage(service, task(candidate), new ByteArrayInputStream(candidate), () -> true));
assertThrows(IOException.class, () -> service.stage(task(candidate), new ByteArrayInputStream(candidate), () -> true));
assertArrayEquals(original, Files.readAllBytes(target));
}
@@ -331,7 +329,7 @@ class PluginDeploymentServiceTest {
Files.writeString(marker.resolve("keep"), "marker publication must fail");
PluginDeploymentService service = PluginDeploymentService.proxy(target);
assertThrows(IOException.class, () -> PluginDeploymentServiceAccess.stage(service, task(candidate), new ByteArrayInputStream(candidate), () -> true));
assertThrows(IOException.class, () -> service.stage(task(candidate), new ByteArrayInputStream(candidate), () -> true));
assertArrayEquals(original, Files.readAllBytes(target));
assertArrayEquals(original, Files.readAllBytes(directory.resolve("VotingPlugin.jar.control-backup")));
@@ -343,7 +341,7 @@ class PluginDeploymentServiceTest {
BlockingInputStream stalled = new BlockingInputStream(active);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<PluginDeploymentService.Result> result = executor.submit(() -> PluginDeploymentServiceAccess.stage(service,
Future<PluginDeploymentService.Result> result = executor.submit(() -> service.stage(
task(new byte[] { 1 }), stalled, active::get));
assertTrue(stalled.awaitRead());
service.cancel();
@@ -1,4 +1,4 @@
package com.bencodez.votingplugin.tests.core;
package com.bencodez.votingplugin.core;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.core.maintenance;
import com.bencodez.votingplugin.core.maintenance.MaintenanceConfirmation;
package com.bencodez.votingplugin.core.maintenance;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.concurrent.atomic.AtomicLong;
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.core.vote;
import com.bencodez.votingplugin.core.vote.SharedVoteDelivery;
package com.bencodez.votingplugin.core.vote;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -1,7 +1,4 @@
package com.bencodez.votingplugin.tests.core.vote;
import com.bencodez.votingplugin.core.vote.SharedVotePolicy;
import com.bencodez.votingplugin.core.vote.SharedVoteProcessor;
package com.bencodez.votingplugin.core.vote;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.events;
import com.bencodez.votingplugin.events.PlayerVoteEvent;
package com.bencodez.votingplugin.events;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.events;
import com.bencodez.votingplugin.events.VoteShopPurchaseEvent;
package com.bencodez.votingplugin.events;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.listeners;
import com.bencodez.votingplugin.listeners.PlayerJoinEvent;
package com.bencodez.votingplugin.listeners;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.listeners;
import com.bencodez.votingplugin.listeners.PlayerJoinEvent;
package com.bencodez.votingplugin.listeners;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -1,4 +1,4 @@
package com.bencodez.votingplugin.tests.listeners;
package com.bencodez.votingplugin.listeners;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.listeners;
import com.bencodez.votingplugin.listeners.PlayerVoteListener;
package com.bencodez.votingplugin.listeners;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -20,7 +18,7 @@ class PlayerVoteListenerProxyVoteIdTest {
PlayerVoteEvent event = new PlayerVoteEvent(null, "Ben", "Example", true);
event.setBungeeTextTotals(new VoteTotalsSnapshot(1, 2, 3, 4, 5, 6, 7, 8, legacyId));
assertEquals(legacyId, PlayerVoteListenerAccess.resolveProxyVoteId(event));
assertEquals(legacyId, PlayerVoteListener.resolveProxyVoteId(event));
}
@Test
@@ -31,6 +29,6 @@ class PlayerVoteListenerProxyVoteIdTest {
event.setProxyVoteId(explicitId);
event.setBungeeTextTotals(new VoteTotalsSnapshot(1, 2, 3, 4, 5, 6, 7, 8, UUID.randomUUID()));
assertEquals(explicitId, PlayerVoteListenerAccess.resolveProxyVoteId(event));
assertEquals(explicitId, PlayerVoteListener.resolveProxyVoteId(event));
}
}
@@ -1,16 +1,4 @@
package com.bencodez.votingplugin.tests.neoforge;
import com.bencodez.votingplugin.neoforge.NeoForgeDeferredVote;
import com.bencodez.votingplugin.neoforge.NeoForgeDeferredVoteStore;
import com.bencodez.votingplugin.neoforge.NeoForgeRewardReplayService;
import com.bencodez.votingplugin.neoforge.NeoForgeRuntime;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteAccount;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteAccountingDecision;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteAccountingStore;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteProcessor;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteRequest;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteResult;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteSite;
package com.bencodez.votingplugin.neoforge;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -55,13 +43,13 @@ class NeoForgeDeferredVoteStoreTest {
writeConfiguration();
UUID historical = UUID.randomUUID();
UUID pending = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.storage().user(historical).write(UserStorage.SQLITE,
NeoForgeVoteAccountingStoreAccess.PLAYER_NAME, new DataValueString("Historical"));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(pending, "Pending", true));
NeoForgeVoteAccountingStore.PLAYER_NAME, new DataValueString("Historical"));
runtime.players().joined(new SharedVoteIdentity(pending, "Pending", true));
runtime.voteProcessor().process(complete(UUID.randomUUID(), pending, 100L));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
assertEquals(List.of(pending), runtime.deferredVotes().users());
}
}
@@ -71,13 +59,13 @@ class NeoForgeDeferredVoteStoreTest {
writeConfiguration();
UUID first = new UUID(0L, 1L);
UUID second = new UUID(0L, 2L);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(first, "First", true));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(second, "Second", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(first, "First", true));
runtime.players().joined(new SharedVoteIdentity(second, "Second", true));
runtime.voteProcessor().process(complete(UUID.randomUUID(), first, "First", 100L));
runtime.voteProcessor().process(complete(UUID.randomUUID(), second, "Second", 100L));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(runtime.storage(), 1, 1);
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(runtime.storage(), 1, 1);
assertEquals(Set.of(first, second), Set.copyOf(bounded.users()));
}
}
@@ -87,17 +75,17 @@ class NeoForgeDeferredVoteStoreTest {
writeConfiguration();
UUID pending = UUID.randomUUID();
Path database;
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
runtime.rewardReplay().ifPresent(replay -> NeoForgeRewardReplayServiceAccess.stopAdmission(replay));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(pending, "Pending", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.rewardReplay().ifPresent(NeoForgeRewardReplayService::stopAdmission);
runtime.players().joined(new SharedVoteIdentity(pending, "Pending", true));
runtime.voteProcessor().process(complete(UUID.randomUUID(), pending, 100L));
database = ((com.bencodez.advancedcore.core.user.storage.sql.SqliteUserBackend)
runtime.storage()).databaseFile();
}
insertHistoricalUsers(database, 5_000);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
runtime.rewardReplay().ifPresent(replay -> NeoForgeRewardReplayServiceAccess.stopAdmission(replay));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.rewardReplay().ifPresent(NeoForgeRewardReplayService::stopAdmission);
assertTimeoutPreemptively(Duration.ofSeconds(2),
() -> assertEquals(List.of(pending), runtime.deferredVotes().users()));
}
@@ -109,8 +97,8 @@ class NeoForgeDeferredVoteStoreTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
Clock clock = Clock.fixed(Instant.parse("2026-09-26T12:00:00Z"), ZoneOffset.UTC);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory, clock)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory, clock)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
runtime.voteProcessor().process(complete(voteId, playerId, 100L)).status());
@@ -125,7 +113,7 @@ class NeoForgeDeferredVoteStoreTest {
runtime.deferredVotes().state(playerId, voteId));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory, clock)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory, clock)) {
assertTrue(runtime.deferredVotes().pending(playerId).isEmpty());
assertEquals(NeoForgeDeferredVoteStore.OccurrenceState.COMPLETED,
runtime.deferredVotes().state(playerId, voteId));
@@ -142,15 +130,15 @@ class NeoForgeDeferredVoteStoreTest {
writeConfiguration();
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
runtime.voteProcessor().process(complete(voteId, playerId, 100L));
NeoForgeDeferredVoteStore.Claim claim = runtime.deferredVotes().claim(playerId, voteId).orElseThrow();
assertTrue(runtime.deferredVotes().claim(playerId, voteId).isEmpty());
claim.close();
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
assertEquals(NeoForgeDeferredVoteStore.OccurrenceState.PENDING,
runtime.deferredVotes().state(playerId, voteId));
assertTrue(runtime.deferredVotes().claim(playerId, voteId).isPresent());
@@ -162,26 +150,26 @@ class NeoForgeDeferredVoteStoreTest {
writeConfiguration();
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
runtime.voteProcessor().process(complete(voteId, playerId, 100L)).status());
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.RELEASED,
NeoForgeDeferredVoteStoreAccess.release(runtime.deferredVotes(), playerId, voteId));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.RELEASED,
runtime.deferredVotes().release(playerId, voteId));
assertTrue(runtime.deferredVotes().pending(playerId).get(0).releaseRequested());
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.ALREADY_RELEASED,
NeoForgeDeferredVoteStoreAccess.release(runtime.deferredVotes(), playerId, voteId));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.ALREADY_RELEASED,
runtime.deferredVotes().release(playerId, voteId));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeDeferredVoteStore store = runtime.deferredVotes();
assertTrue(store.pending(playerId).get(0).releaseRequested());
try (NeoForgeDeferredVoteStore.Claim claim = store.claim(playerId, voteId).orElseThrow()) {
assertEquals(NeoForgeDeferredVoteStore.CompletionResult.COMPLETED, claim.complete());
}
assertTrue(store.pending(playerId).isEmpty());
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.ALREADY_RELEASED,
NeoForgeDeferredVoteStoreAccess.release(store, playerId, voteId));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.ALREADY_RELEASED,
store.release(playerId, voteId));
assertNoAccounting(runtime.accounting().load(playerId).orElseThrow());
}
}
@@ -191,20 +179,20 @@ class NeoForgeDeferredVoteStoreTest {
writeConfiguration();
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
runtime.voteProcessor().process(complete(voteId, playerId, 100L));
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.RELEASED,
NeoForgeDeferredVoteStoreAccess.release(runtime.deferredVotes(), playerId, voteId));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.RELEASED,
runtime.deferredVotes().release(playerId, voteId));
try (NeoForgeDeferredVoteStore.Claim claim = runtime.deferredVotes().claim(playerId, voteId).orElseThrow()) {
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.NOT_COMPLETED,
NeoForgeDeferredVoteStoreAccess.release(runtime.deferredVotes(), playerId, voteId));
assertEquals(NeoForgeDeferredVoteStoreAccess.FenceResult.FENCED, NeoForgeDeferredVoteStoreAccess.fenceExternalEffects(claim));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.NOT_COMPLETED,
runtime.deferredVotes().release(playerId, voteId));
assertEquals(NeoForgeDeferredVoteStore.FenceResult.FENCED, claim.fenceExternalEffects());
}
assertTrue(runtime.deferredVotes().pending(playerId).get(0).releaseRequested());
assertTrue(runtime.deferredVotes().pending(playerId).get(0).quarantined());
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.ALREADY_RELEASED,
NeoForgeDeferredVoteStoreAccess.release(runtime.deferredVotes(), playerId, voteId));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.ALREADY_RELEASED,
runtime.deferredVotes().release(playerId, voteId));
}
}
@@ -214,18 +202,18 @@ class NeoForgeDeferredVoteStoreTest {
UUID playerId = UUID.randomUUID();
UUID firstVote = UUID.randomUUID();
UUID secondVote = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(
runtime.storage(), 2, 2, 2, 2, 1);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), Clock.systemUTC());
processor.process(complete(firstVote, playerId, 100L));
processor.process(complete(secondVote, playerId, 200L));
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.RELEASED,
NeoForgeDeferredVoteStoreAccess.release(bounded, playerId, firstVote));
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.RELEASED,
NeoForgeDeferredVoteStoreAccess.release(bounded, playerId, secondVote));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.RELEASED,
bounded.release(playerId, firstVote));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.RELEASED,
bounded.release(playerId, secondVote));
try (NeoForgeDeferredVoteStore.Claim claim = bounded.claim(playerId, firstVote).orElseThrow()) {
assertTrue(bounded.claim(playerId, secondVote).isEmpty());
// The released receipt reservation blocks the second effect before accounting.
@@ -241,19 +229,19 @@ class NeoForgeDeferredVoteStoreTest {
UUID secondPlayer = UUID.randomUUID();
UUID firstVote = UUID.randomUUID();
UUID secondVote = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(firstPlayer, "First", true));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(secondPlayer, "Second", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(firstPlayer, "First", true));
runtime.players().joined(new SharedVoteIdentity(secondPlayer, "Second", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(
runtime.storage(), 2, 2, 2, 2, 1, 1);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), Clock.systemUTC());
processor.process(complete(firstVote, firstPlayer, "First", 100L));
processor.process(complete(secondVote, secondPlayer, "Second", 200L));
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.RELEASED,
NeoForgeDeferredVoteStoreAccess.release(bounded, firstPlayer, firstVote));
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.RELEASED,
NeoForgeDeferredVoteStoreAccess.release(bounded, secondPlayer, secondVote));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.RELEASED,
bounded.release(firstPlayer, firstVote));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.RELEASED,
bounded.release(secondPlayer, secondVote));
try (NeoForgeDeferredVoteStore.Claim claim = bounded.claim(firstPlayer, firstVote).orElseThrow()) {
assertTrue(bounded.claim(secondPlayer, secondVote).isEmpty());
}
@@ -267,19 +255,19 @@ class NeoForgeDeferredVoteStoreTest {
writeConfiguration();
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
runtime.storage().user(playerId).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES,
NeoForgeDeferredVoteStore.DEFERRED_VOTES,
new DataValueString("v1|" + voteId
+ "|QWxleA|ZXhhbXBsZS50ZXN0|RXhhbXBsZQ|100|true|true|true"));
assertEquals(NeoForgeDeferredVoteStoreAccess.QuarantineResult.QUARANTINED,
NeoForgeDeferredVoteStoreAccess.quarantine(runtime.deferredVotes(), playerId, voteId));
assertEquals(NeoForgeDeferredVoteStore.QuarantineResult.QUARANTINED,
runtime.deferredVotes().quarantine(playerId, voteId));
assertTrue(runtime.deferredVotes().pending(playerId).get(0).quarantined());
assertTrue(runtime.deferredVotes().claim(playerId, voteId).isEmpty());
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeDeferredVote vote = runtime.deferredVotes().pending(playerId).get(0);
assertEquals(voteId, vote.voteId());
assertTrue(vote.quarantined());
@@ -296,10 +284,10 @@ class NeoForgeDeferredVoteStoreTest {
UUID voteId = UUID.randomUUID();
String legacy = "v1|" + voteId
+ "|QWxleA|ZXhhbXBsZS50ZXN0|RXhhbXBsZQ|100|true|true|false";
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.storage().user(playerId).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES, new DataValueString(legacy));
NeoForgeVoteSite site = NeoForgeVoteConfigurationAccess.configuredSite(runtime.voteConfiguration(), "Example").orElseThrow();
NeoForgeDeferredVoteStore.DEFERRED_VOTES, new DataValueString(legacy));
NeoForgeVoteSite site = runtime.voteConfiguration().configuredSite("Example").orElseThrow();
try (NeoForgeDeferredVoteStore.Claim claim = runtime.deferredVotes()
.claim(playerId, voteId).orElseThrow()) {
assertThrows(IllegalStateException.class,
@@ -316,8 +304,8 @@ class NeoForgeDeferredVoteStoreTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
Clock clock = Clock.fixed(Instant.parse("2026-09-26T12:00:00Z"), ZoneOffset.UTC);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory, clock)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory, clock)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
runtime.voteProcessor().process(complete(voteId, playerId, 100L));
String pending = "v1|" + voteId + "|QWxleA|ZXhhbXBsZS50ZXN0|RXhhbXBsZQ|100|true|true|true";
try (NeoForgeDeferredVoteStore.Claim claim = runtime.deferredVotes()
@@ -325,10 +313,10 @@ class NeoForgeDeferredVoteStoreTest {
assertEquals(NeoForgeDeferredVoteStore.CompletionResult.COMPLETED, claim.complete());
}
runtime.storage().user(playerId).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES, new DataValueString(pending));
NeoForgeDeferredVoteStore.DEFERRED_VOTES, new DataValueString(pending));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory, clock)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory, clock)) {
NeoForgeVoteResult retry = runtime.voteProcessor().process(complete(voteId, playerId, 200L));
assertEquals(NeoForgeVoteResult.Status.ALREADY_COMPLETED, retry.status());
assertTrue(runtime.deferredVotes().pending(playerId).isEmpty());
@@ -342,9 +330,9 @@ class NeoForgeDeferredVoteStoreTest {
UUID completedVote = UUID.randomUUID();
UUID otherVote = UUID.randomUUID();
String malformedPending = "not-a-supported-record";
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
SharedVoteIdentity identity = new SharedVoteIdentity(playerId, "Alex", true);
NeoForgePlayerDirectoryAccess.joined(runtime.players(), identity);
runtime.players().joined(identity);
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
runtime.voteProcessor().process(complete(completedVote, playerId, 100L)).status());
try (NeoForgeDeferredVoteStore.Claim claim = runtime.deferredVotes()
@@ -352,15 +340,15 @@ class NeoForgeDeferredVoteStoreTest {
assertEquals(NeoForgeDeferredVoteStore.CompletionResult.COMPLETED, claim.complete());
}
runtime.storage().user(playerId).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES, new DataValueString(malformedPending));
NeoForgeDeferredVoteStore.DEFERRED_VOTES, new DataValueString(malformedPending));
NeoForgeDeferredVoteStore store = runtime.deferredVotes();
NeoForgeVoteSite site = runtime.voteConfiguration().resolveEnabledSite("example.test").orElseThrow();
assertEquals(NeoForgeDeferredVoteStore.OccurrenceState.COMPLETED,
store.state(playerId, completedVote));
assertTrue(store.claim(playerId, completedVote).isEmpty());
assertEquals(NeoForgeDeferredVoteStoreAccess.Status.ALREADY_COMPLETED,
NeoForgeDeferredVoteStoreAccess.defer(store, identity, input(completedVote), site,
assertEquals(NeoForgeDeferredVoteStore.Status.ALREADY_COMPLETED,
store.defer(identity, input(completedVote), site,
accountingDecision(runtime, input(completedVote), site)).status());
assertEquals(NeoForgeVoteResult.Status.ALREADY_COMPLETED,
runtime.voteProcessor().process(complete(completedVote, playerId, 200L)).status());
@@ -369,7 +357,7 @@ class NeoForgeDeferredVoteStoreTest {
assertThrows(IllegalStateException.class, () -> store.pending(playerId));
assertThrows(IllegalStateException.class, () -> store.state(playerId, otherVote));
assertThrows(IllegalStateException.class, () -> store.claim(playerId, otherVote));
assertThrows(IllegalStateException.class, () -> NeoForgeDeferredVoteStoreAccess.defer(store, identity, input(otherVote), site,
assertThrows(IllegalStateException.class, () -> store.defer(identity, input(otherVote), site,
accountingDecision(runtime, input(otherVote), site)));
assertEquals(malformedPending, storedPending(runtime, playerId));
}
@@ -381,15 +369,15 @@ class NeoForgeDeferredVoteStoreTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
String malformedPending = "not-a-supported-record";
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
runtime.voteProcessor().process(complete(voteId, playerId, 100L));
try (NeoForgeDeferredVoteStore.Claim claim = runtime.deferredVotes()
.claim(playerId, voteId).orElseThrow()) {
runtime.storage().user(playerId).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES, new DataValueString(malformedPending));
NeoForgeDeferredVoteStore.DEFERRED_VOTES, new DataValueString(malformedPending));
runtime.storage().user(playerId).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.COMPLETED_DEFERRED_VOTES,
NeoForgeDeferredVoteStore.COMPLETED_DEFERRED_VOTES,
new DataValueString("v1|" + voteId));
assertEquals(NeoForgeDeferredVoteStore.CompletionResult.ALREADY_COMPLETED, claim.complete());
}
@@ -404,13 +392,13 @@ class NeoForgeDeferredVoteStoreTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
String malformedPending = "not-a-supported-record";
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
runtime.voteProcessor().process(complete(voteId, playerId, 100L));
try (NeoForgeDeferredVoteStore.Claim claim = runtime.deferredVotes()
.claim(playerId, voteId).orElseThrow()) {
runtime.storage().user(playerId).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES, new DataValueString(malformedPending));
NeoForgeDeferredVoteStore.DEFERRED_VOTES, new DataValueString(malformedPending));
assertThrows(IllegalStateException.class, claim::complete);
}
assertEquals(malformedPending, storedPending(runtime, playerId));
@@ -424,11 +412,11 @@ class NeoForgeDeferredVoteStoreTest {
UUID firstId = UUID.randomUUID();
UUID secondId = UUID.randomUUID();
Clock clock = Clock.fixed(Instant.parse("2026-09-26T12:00:00Z"), ZoneOffset.UTC);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory, clock)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory, clock)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(
runtime.storage(), 1, 1, 1, 1);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), clock);
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
@@ -439,16 +427,16 @@ class NeoForgeDeferredVoteStoreTest {
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
processor.process(complete(secondId, playerId, 200L)).status());
assertTrue(bounded.claim(playerId, secondId).isEmpty());
assertEquals(NeoForgeDeferredVoteStoreAccess.ClaimStatus.RECEIPT_CAPACITY_REACHED,
NeoForgeDeferredVoteStoreAccess.claimForReplay(bounded, playerId, secondId).status());
assertEquals(NeoForgeDeferredVoteStore.ClaimStatus.RECEIPT_CAPACITY_REACHED,
bounded.claimForReplay(playerId, secondId).status());
assertEquals(List.of(secondId), bounded.pending(playerId).stream()
.map(NeoForgeDeferredVote::voteId).toList());
assertEquals(NeoForgeDeferredVoteStore.OccurrenceState.COMPLETED,
bounded.state(playerId, firstId));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory, clock)) {
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory, clock)) {
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(
runtime.storage(), 1, 1, 1, 1);
assertEquals(NeoForgeDeferredVoteStore.OccurrenceState.COMPLETED,
bounded.state(playerId, firstId));
@@ -465,11 +453,11 @@ class NeoForgeDeferredVoteStoreTest {
UUID secondPlayer = UUID.randomUUID();
UUID firstVote = UUID.randomUUID();
UUID secondVote = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(firstPlayer, "First", true));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(secondPlayer, "Second", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(runtime.storage(), 1, 2, 1, 1);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(firstPlayer, "First", true));
runtime.players().joined(new SharedVoteIdentity(secondPlayer, "Second", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(runtime.storage(), 1, 2, 1, 1);
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), Clock.systemUTC());
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
processor.process(complete(firstVote, firstPlayer, "First", 100L)).status());
@@ -509,8 +497,8 @@ class NeoForgeDeferredVoteStoreTest {
assertTrue(bounded.claim(firstClaim.isPresent() ? firstPlayer : secondPlayer,
firstClaim.isPresent() ? firstVote : secondVote).isEmpty());
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(runtime.storage(), 1, 2, 1, 1);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(runtime.storage(), 1, 2, 1, 1);
assertEquals(1, bounded.pending(firstPlayer).size() + bounded.pending(secondPlayer).size());
assertTrue(bounded.claim(firstPlayer, firstVote).isEmpty());
assertTrue(bounded.claim(secondPlayer, secondVote).isEmpty());
@@ -525,11 +513,11 @@ class NeoForgeDeferredVoteStoreTest {
UUID firstVote = UUID.randomUUID();
UUID secondVote = UUID.randomUUID();
UUID otherVote = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(firstPlayer, "First", true));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(secondPlayer, "Second", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(runtime.storage(), 2, 3, 1, 2);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(firstPlayer, "First", true));
runtime.players().joined(new SharedVoteIdentity(secondPlayer, "Second", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(runtime.storage(), 2, 3, 1, 2);
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), Clock.systemUTC());
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
processor.process(complete(firstVote, firstPlayer, "First", 100L)).status());
@@ -556,8 +544,8 @@ class NeoForgeDeferredVoteStoreTest {
UUID voteId = UUID.randomUUID();
Instant start = Instant.parse("2026-09-26T12:00:00Z");
Clock clock = Clock.fixed(start, ZoneOffset.UTC);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory, clock)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory, clock)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
runtime.voteProcessor().process(complete(voteId, playerId, 100L));
try (NeoForgeDeferredVoteStore.Claim claim = runtime.deferredVotes()
.claim(playerId, voteId).orElseThrow()) {
@@ -566,8 +554,8 @@ class NeoForgeDeferredVoteStoreTest {
}
Clock muchLater = Clock.fixed(start.plusSeconds(365L * 24L * 60L * 60L), ZoneOffset.UTC);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory, muchLater)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory, muchLater)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
assertEquals(NeoForgeDeferredVoteStore.OccurrenceState.COMPLETED,
runtime.deferredVotes().state(playerId, voteId));
assertEquals(NeoForgeVoteResult.Status.ALREADY_COMPLETED,
@@ -581,8 +569,8 @@ class NeoForgeDeferredVoteStoreTest {
UUID playerId = UUID.randomUUID();
UUID firstId = UUID.randomUUID();
UUID secondId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
runtime.voteProcessor().process(complete(firstId, playerId, 100L)).status());
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
@@ -595,7 +583,7 @@ class NeoForgeDeferredVoteStoreTest {
assertEquals(100L, runtime.deferredVotes().pending(playerId).get(0).voteTime());
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
List<NeoForgeDeferredVote> pending = runtime.deferredVotes().pending(playerId);
assertEquals(List.of(firstId, secondId), pending.stream().map(NeoForgeDeferredVote::voteId).toList());
assertEquals(100L, pending.get(0).voteTime());
@@ -608,9 +596,9 @@ class NeoForgeDeferredVoteStoreTest {
void capacityFailureIsExplicitAndDoesNotMutateAccounting() throws IOException {
writeConfiguration();
UUID playerId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
for (int index = 0; index < NeoForgeDeferredVoteStoreAccess.MAX_DEFERRED_PER_USER; index++) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
for (int index = 0; index < NeoForgeDeferredVoteStore.MAX_DEFERRED_PER_USER; index++) {
assertEquals(NeoForgeVoteResult.Status.DEFERRED, runtime.voteProcessor()
.process(complete(new UUID(0, index + 1L), playerId, index + 1L)).status());
}
@@ -620,7 +608,7 @@ class NeoForgeDeferredVoteStoreTest {
assertEquals(NeoForgeVoteResult.Status.DEFERRED_CAPACITY_REACHED, full.status());
assertFalse(full.accountingMutated());
assertFalse(full.durablyRetained());
assertEquals(NeoForgeDeferredVoteStoreAccess.MAX_DEFERRED_PER_USER,
assertEquals(NeoForgeDeferredVoteStore.MAX_DEFERRED_PER_USER,
runtime.deferredVotes().pending(playerId).size());
assertNoAccounting(runtime.accounting().load(playerId).orElseThrow());
}
@@ -632,11 +620,11 @@ class NeoForgeDeferredVoteStoreTest {
UUID first = UUID.randomUUID();
UUID second = UUID.randomUUID();
UUID rejected = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(first, "First", true));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(second, "Second", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(runtime.storage(), 2, 2);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(first, "First", true));
runtime.players().joined(new SharedVoteIdentity(second, "Second", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(runtime.storage(), 2, 2);
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), Clock.systemUTC());
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
@@ -645,10 +633,10 @@ class NeoForgeDeferredVoteStoreTest {
processor.process(complete(UUID.randomUUID(), second, "Second", 200L)).status());
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(rejected, "Rejected", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(runtime.storage(), 2, 2);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(rejected, "Rejected", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(runtime.storage(), 2, 2);
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), Clock.systemUTC());
NeoForgeVoteResult full = processor.process(
complete(UUID.randomUUID(), rejected, "Rejected", 300L));
@@ -664,8 +652,8 @@ class NeoForgeDeferredVoteStoreTest {
writeConfiguration();
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
runtime.voteProcessor().process(complete(voteId, playerId, 100L)).status());
}
@@ -676,7 +664,7 @@ class NeoForgeDeferredVoteStoreTest {
ServiceSite: renamed.test
""");
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteResult retry = runtime.voteProcessor().process(complete(voteId, playerId, 999L));
assertEquals(NeoForgeVoteResult.Status.DEFERRED, retry.status());
assertTrue(retry.durablyRetained());
@@ -690,9 +678,9 @@ class NeoForgeDeferredVoteStoreTest {
void malformedStoredQueueIsNotSilentlyOverwritten() throws IOException {
writeConfiguration();
UUID playerId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.storage().user(playerId).write(UserStorage.SQLITE, NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES,
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
runtime.storage().user(playerId).write(UserStorage.SQLITE, NeoForgeDeferredVoteStore.DEFERRED_VOTES,
new DataValueString("not-a-supported-record"));
assertThrows(IllegalStateException.class,
@@ -707,8 +695,8 @@ class NeoForgeDeferredVoteStoreTest {
writeConfiguration();
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
runtime.storage().user(playerId).write(UserStorage.SQLITE, NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES,
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.storage().user(playerId).write(UserStorage.SQLITE, NeoForgeDeferredVoteStore.DEFERRED_VOTES,
new DataValueString("v1|" + voteId
+ "|_w|ZXhhbXBsZS50ZXN0|RXhhbXBsZQ|100|true|true|true"));
@@ -723,15 +711,15 @@ class NeoForgeDeferredVoteStoreTest {
writeConfiguration();
UUID corruptPlayer = UUID.randomUUID();
UUID healthyPlayer = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.storage().user(corruptPlayer).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES, new DataValueString("not-a-supported-record"));
NeoForgeDeferredVoteStore.DEFERRED_VOTES, new DataValueString("not-a-supported-record"));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(healthyPlayer, "Healthy", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(runtime.storage(), 2, 3);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(healthyPlayer, "Healthy", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(runtime.storage(), 2, 3);
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), Clock.systemUTC());
NeoForgeVoteResult result = processor.process(
@@ -749,16 +737,16 @@ class NeoForgeDeferredVoteStoreTest {
writeConfiguration();
UUID corruptPlayer = UUID.randomUUID();
UUID healthyPlayer = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.storage().user(corruptPlayer).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.COMPLETED_DEFERRED_VOTES,
NeoForgeDeferredVoteStore.COMPLETED_DEFERRED_VOTES,
new DataValueString("not-a-supported-receipt"));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(healthyPlayer, "Healthy", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(runtime.storage(), 2, 3, 2, 3);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(healthyPlayer, "Healthy", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(runtime.storage(), 2, 3, 2, 3);
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), Clock.systemUTC());
assertEquals(NeoForgeVoteResult.Status.DEFERRED, processor.process(
@@ -774,12 +762,12 @@ class NeoForgeDeferredVoteStoreTest {
UUID playerId = UUID.randomUUID();
UUID firstVote = UUID.randomUUID();
UUID secondVote = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
runtime.rewardReplay().ifPresent(replay -> NeoForgeRewardReplayServiceAccess.stopAdmission(replay));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.rewardReplay().ifPresent(NeoForgeRewardReplayService::stopAdmission);
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(
runtime.storage(), 2, 2, 2, 2, 1);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), Clock.systemUTC());
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
@@ -793,23 +781,23 @@ class NeoForgeDeferredVoteStoreTest {
assertEquals(NeoForgeDeferredVoteStore.CompletionResult.COMPLETED, claim.complete());
}
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.RELEASED,
NeoForgeDeferredVoteStoreAccess.release(bounded, playerId, firstVote));
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.RELEASE_CAPACITY_REACHED,
NeoForgeDeferredVoteStoreAccess.release(bounded, playerId, secondVote));
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.ALREADY_RELEASED,
NeoForgeDeferredVoteStoreAccess.release(bounded, playerId, firstVote));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.RELEASED,
bounded.release(playerId, firstVote));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.RELEASE_CAPACITY_REACHED,
bounded.release(playerId, secondVote));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.ALREADY_RELEASED,
bounded.release(playerId, firstVote));
assertEquals(NeoForgeDeferredVoteStore.OccurrenceState.COMPLETED,
bounded.state(playerId, secondVote));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(
runtime.storage(), 2, 2, 2, 2, 1);
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.ALREADY_RELEASED,
NeoForgeDeferredVoteStoreAccess.release(bounded, playerId, firstVote));
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.RELEASE_CAPACITY_REACHED,
NeoForgeDeferredVoteStoreAccess.release(bounded, playerId, secondVote));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.ALREADY_RELEASED,
bounded.release(playerId, firstVote));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.RELEASE_CAPACITY_REACHED,
bounded.release(playerId, secondVote));
assertEquals(NeoForgeDeferredVoteStore.OccurrenceState.COMPLETED,
bounded.state(playerId, secondVote));
}
@@ -822,30 +810,30 @@ class NeoForgeDeferredVoteStoreTest {
UUID secondPlayer = UUID.randomUUID();
UUID firstVote = UUID.randomUUID();
UUID secondVote = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.storage().user(firstPlayer).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.COMPLETED_DEFERRED_VOTES,
NeoForgeDeferredVoteStore.COMPLETED_DEFERRED_VOTES,
new DataValueString("v2|" + firstVote + "|" + System.currentTimeMillis()));
runtime.storage().user(secondPlayer).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.COMPLETED_DEFERRED_VOTES,
NeoForgeDeferredVoteStore.COMPLETED_DEFERRED_VOTES,
new DataValueString("v1|" + secondVote));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(
runtime.storage(), 2, 2, 2, 2, 2, 1);
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.RELEASE_CAPACITY_REACHED,
NeoForgeDeferredVoteStoreAccess.release(bounded, secondPlayer, secondVote));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.RELEASE_CAPACITY_REACHED,
bounded.release(secondPlayer, secondVote));
runtime.storage().user(firstPlayer).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.COMPLETED_DEFERRED_VOTES,
NeoForgeDeferredVoteStore.COMPLETED_DEFERRED_VOTES,
new DataValueString("v2|" + firstVote + "|"
+ (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(8))));
NeoForgeDeferredVoteStore restarted = NeoForgeDeferredVoteStoreAccess.create(
NeoForgeDeferredVoteStore restarted = new NeoForgeDeferredVoteStore(
runtime.storage(), 2, 2, 2, 2, 2, 1);
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.RELEASED,
NeoForgeDeferredVoteStoreAccess.release(restarted, secondPlayer, secondVote));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.RELEASED,
restarted.release(secondPlayer, secondVote));
String firstReceipts = runtime.storage().user(firstPlayer).readRow(UserStorage.SQLITE).stream()
.filter(column -> column.getName().equalsIgnoreCase(
NeoForgeDeferredVoteStoreAccess.COMPLETED_DEFERRED_VOTES))
NeoForgeDeferredVoteStore.COMPLETED_DEFERRED_VOTES))
.findFirst().orElseThrow().getValue().getString();
assertEquals("", firstReceipts);
}
@@ -857,23 +845,23 @@ class NeoForgeDeferredVoteStoreTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
long expired = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(8);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.storage().user(playerId).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.COMPLETED_DEFERRED_VOTES,
NeoForgeDeferredVoteStore.COMPLETED_DEFERRED_VOTES,
new DataValueString("v2|" + voteId + "|" + expired));
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.ALREADY_RELEASED,
NeoForgeDeferredVoteStoreAccess.release(runtime.deferredVotes(), playerId, voteId));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.ALREADY_RELEASED,
runtime.deferredVotes().release(playerId, voteId));
String renewed = runtime.storage().user(playerId).readRow(UserStorage.SQLITE).stream()
.filter(column -> column.getName().equalsIgnoreCase(
NeoForgeDeferredVoteStoreAccess.COMPLETED_DEFERRED_VOTES))
NeoForgeDeferredVoteStore.COMPLETED_DEFERRED_VOTES))
.findFirst().orElseThrow().getValue().getString();
assertTrue(Long.parseLong(renewed.split("\\|", -1)[2]) > expired);
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.ALREADY_RELEASED,
NeoForgeDeferredVoteStoreAccess.release(runtime.deferredVotes(), playerId, voteId));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.ALREADY_RELEASED,
runtime.deferredVotes().release(playerId, voteId));
}
}
@@ -882,16 +870,16 @@ class NeoForgeDeferredVoteStoreTest {
writeConfiguration();
UUID corruptPlayer = UUID.randomUUID();
UUID healthyPlayer = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.storage().user(corruptPlayer).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.COMPLETED_DEFERRED_VOTES,
NeoForgeDeferredVoteStore.COMPLETED_DEFERRED_VOTES,
new DataValueString("not-a-supported-receipt"));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(healthyPlayer, "Healthy", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(runtime.storage(), 1, 1, 1, 1);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(healthyPlayer, "Healthy", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(runtime.storage(), 1, 1, 1, 1);
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), Clock.systemUTC());
assertEquals(NeoForgeVoteResult.Status.DEFERRED, processor.process(
@@ -907,16 +895,16 @@ class NeoForgeDeferredVoteStoreTest {
UUID corruptPlayer = UUID.randomUUID();
UUID healthyPlayer = UUID.randomUUID();
UUID healthyVote = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.storage().user(corruptPlayer).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES,
NeoForgeDeferredVoteStore.DEFERRED_VOTES,
new DataValueString("not-a-supported-record"));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(healthyPlayer, "Healthy", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(runtime.storage(), 1, 2, 1, 1);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(healthyPlayer, "Healthy", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(runtime.storage(), 1, 2, 1, 1);
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), Clock.systemUTC());
assertEquals(NeoForgeVoteResult.Status.DEFERRED,
processor.process(complete(healthyVote, healthyPlayer, "Healthy", 100L)).status());
@@ -952,7 +940,7 @@ class NeoForgeDeferredVoteStoreTest {
}
private static void insertHistoricalUsers(Path database, int count) throws SQLException {
String sql = "INSERT INTO `" + NeoForgeRuntimeAccess.USER_TABLE_NAME
String sql = "INSERT INTO `" + NeoForgeRuntime.USER_TABLE_NAME
+ "` (`UUID`, `PlayerName`) VALUES (?, ?)";
try (Connection connection = DriverManager.getConnection("jdbc:sqlite:" + database.toAbsolutePath());
PreparedStatement statement = connection.prepareStatement(sql)) {
@@ -978,13 +966,13 @@ class NeoForgeDeferredVoteStoreTest {
private static NeoForgeVoteAccountingDecision accountingDecision(NeoForgeRuntime runtime,
SharedVoteInput input, NeoForgeVoteSite site) {
return NeoForgeVoteAccountingDecisionAccess.capture(input, runtime.voteConfiguration().policyFor(site),
return NeoForgeVoteAccountingDecision.capture(input, runtime.voteConfiguration().policyFor(site),
true, runtime.voteConfiguration().pointsOnVote(), runtime.voteConfiguration().limitVotePoints());
}
private static String storedPending(NeoForgeRuntime runtime, UUID playerId) {
return runtime.storage().user(playerId).readRow(UserStorage.SQLITE).stream()
.filter(column -> column.getName().equalsIgnoreCase(NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES))
.filter(column -> column.getName().equalsIgnoreCase(NeoForgeDeferredVoteStore.DEFERRED_VOTES))
.findFirst().orElseThrow().getValue().getString();
}
@@ -1,14 +1,4 @@
package com.bencodez.votingplugin.tests.neoforge;
import com.bencodez.votingplugin.neoforge.NeoForgeDeferredVote;
import com.bencodez.votingplugin.neoforge.NeoForgeDeferredVoteStore;
import com.bencodez.votingplugin.tests.neoforge.NeoForgeProxySocketConfigurationAccess;
import com.bencodez.votingplugin.neoforge.NeoForgeProxySocketService;
import com.bencodez.votingplugin.neoforge.NeoForgeRewardReplayService;
import com.bencodez.votingplugin.neoforge.NeoForgeRuntime;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteProcessor;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteRequest;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteResult;
package com.bencodez.votingplugin.neoforge;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -81,14 +71,14 @@ class NeoForgeProxySocketServiceTest {
@Test
void commonBungeeAndVelocityEnvelopeIsRetainedBeforeAcknowledgement() throws IOException {
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
for (String platform : List.of("BungeeCord", "Velocity")) {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(voteId, playerId, false)));
service.receive(fromProxy(vote(voteId, playerId, false)));
assertEquals(1, runtime.deferredVotes().pending(playerId).size(), platform);
assertEquals(voteId, runtime.deferredVotes().pending(playerId).get(0).voteId());
assertTrue(runtime.deferredVotes().pending(playerId).get(0).wasOnline());
@@ -104,22 +94,22 @@ class NeoForgeProxySocketServiceTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
JsonEnvelope vote = vote(voteId, playerId, false);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote));
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote));
service.receive(fromProxy(vote));
service.receive(fromProxy(vote));
assertEquals(1, runtime.deferredVotes().pending(playerId).size());
assertEquals(2, sent.stream().filter(message -> VotingPluginWire.SUB_VOTE_DELIVERY_ACK
.equals(message.getSubChannel())).count());
}
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote));
service.receive(fromProxy(vote));
assertEquals(1, runtime.deferredVotes().pending(playerId).size());
assertEquals(VotingPluginWire.SUB_VOTE_DELIVERY_ACK, only(sent).getSubChannel());
}
@@ -131,22 +121,22 @@ class NeoForgeProxySocketServiceTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
JsonEnvelope vote = vote(voteId, playerId, false);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote));
service.receive(fromProxy(vote));
assertEquals(1, runtime.deferredVotes().pending(playerId).size());
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(VotingPluginWire.voteDeliveryReceiptRelease(
service.receive(fromProxy(VotingPluginWire.voteDeliveryReceiptRelease(
"neoforge", voteId, VotingPluginWire.SUB_VOTE, playerId.toString())));
assertEquals(VotingPluginWire.SUB_VOTE_DELIVERY_RECEIPT_RELEASE_ACK,
only(sent).getSubChannel());
assertTrue(runtime.deferredVotes().pending(playerId).get(0).releaseRequested());
assertEquals(NeoForgeDeferredVoteStoreAccess.ReleaseResult.ALREADY_RELEASED,
NeoForgeDeferredVoteStoreAccess.release(runtime.deferredVotes(), playerId, voteId));
assertEquals(NeoForgeDeferredVoteStore.ReleaseResult.ALREADY_RELEASED,
runtime.deferredVotes().release(playerId, voteId));
}
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
assertTrue(runtime.deferredVotes().pending(playerId).get(0).releaseRequested());
assertEquals(NeoForgeDeferredVoteStore.OccurrenceState.PENDING,
runtime.deferredVotes().state(playerId, voteId));
@@ -158,11 +148,11 @@ class NeoForgeProxySocketServiceTest {
UUID playerId = UUID.randomUUID();
UUID firstVoteId = UUID.randomUUID();
UUID secondVoteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(firstVoteId, playerId, false, 1, 2)));
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(secondVoteId, playerId, false, 2, 2)));
service.receive(fromProxy(vote(firstVoteId, playerId, false, 1, 2)));
service.receive(fromProxy(vote(secondVoteId, playerId, false, 2, 2)));
assertEquals(List.of(firstVoteId, secondVoteId), runtime.deferredVotes().pending(playerId)
.stream().map(NeoForgeDeferredVote::voteId).toList());
@@ -175,13 +165,13 @@ class NeoForgeProxySocketServiceTest {
@Test
void invalidCachedVoteBatchMetadataIsNotAcknowledged() throws IOException {
UUID playerId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(UUID.randomUUID(), playerId, false, 0, 2)));
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(UUID.randomUUID(), playerId, false, 3, 2)));
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(UUID.randomUUID(), playerId, false, 1, 0)));
service.receive(fromProxy(vote(UUID.randomUUID(), playerId, false, 0, 2)));
service.receive(fromProxy(vote(UUID.randomUUID(), playerId, false, 3, 2)));
service.receive(fromProxy(vote(UUID.randomUUID(), playerId, false, 1, 0)));
assertTrue(sent.isEmpty());
assertTrue(runtime.deferredVotes().pending(playerId).isEmpty());
}
@@ -191,12 +181,12 @@ class NeoForgeProxySocketServiceTest {
@Test
void proxyBroadcastVoteIsNeitherRetainedNorAcknowledged() throws IOException {
UUID playerId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
UUID voteId = UUID.randomUUID();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(voteId, playerId, false, true)));
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(VotingPluginWire.requestVoteDeliveryAcknowledgement(
service.receive(fromProxy(vote(voteId, playerId, false, true)));
service.receive(fromProxy(VotingPluginWire.requestVoteDeliveryAcknowledgement(
VotingPluginWire.voteOnline("Alex", playerId.toString(), "Service", 100L,
true, true, "", UUID.randomUUID(), false, true, 1, 1))));
assertTrue(runtime.deferredVotes().pending(playerId).isEmpty());
@@ -211,20 +201,20 @@ class NeoForgeProxySocketServiceTest {
void completedRetryAcknowledgesWithoutRepeatingAdmissionAndReleaseKeepsTombstone() throws IOException {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(voteId, playerId, false)));
service.receive(fromProxy(vote(voteId, playerId, false)));
try (NeoForgeDeferredVoteStore.Claim claim = runtime.deferredVotes()
.claim(playerId, voteId).orElseThrow()) {
assertEquals(NeoForgeDeferredVoteStore.CompletionResult.COMPLETED, claim.complete());
}
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(voteId, playerId, false)));
service.receive(fromProxy(vote(voteId, playerId, false)));
assertEquals(VotingPluginWire.SUB_VOTE_DELIVERY_ACK, only(sent).getSubChannel());
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(VotingPluginWire.voteDeliveryReceiptRelease("neoforge", voteId,
service.receive(fromProxy(VotingPluginWire.voteDeliveryReceiptRelease("neoforge", voteId,
VotingPluginWire.SUB_VOTE, playerId.toString())));
assertEquals(VotingPluginWire.SUB_VOTE_DELIVERY_RECEIPT_RELEASE_ACK,
only(sent).getSubChannel());
@@ -240,21 +230,21 @@ class NeoForgeProxySocketServiceTest {
UUID secondPlayer = UUID.randomUUID();
UUID firstVote = UUID.randomUUID();
UUID secondVote = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
runtime.rewardReplay().ifPresent(replay -> NeoForgeRewardReplayServiceAccess.stopAdmission(replay));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(runtime.storage(), 1, 2, 1, 1);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.rewardReplay().ifPresent(NeoForgeRewardReplayService::stopAdmission);
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(runtime.storage(), 1, 2, 1, 1);
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), java.time.Clock.systemUTC());
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new com.bencodez.votingplugin.core.vote.SharedVoteIdentity(
runtime.players().joined(new com.bencodez.votingplugin.core.vote.SharedVoteIdentity(
firstPlayer, "First", true));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new com.bencodez.votingplugin.core.vote.SharedVoteIdentity(
runtime.players().joined(new com.bencodez.votingplugin.core.vote.SharedVoteIdentity(
secondPlayer, "Second", true));
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = NeoForgeProxySocketServiceAccess.create(
new NeoForgeProxySocketConfigurationAccess(true, "neoforge", "proxy1", "127.0.0.1", 1297,
try (NeoForgeProxySocketService service = new NeoForgeProxySocketService(
new NeoForgeProxySocketConfiguration(true, "neoforge", "proxy1", "127.0.0.1", 1297,
"127.0.0.1", 1298, "socket-auth.key", false, false),
processor, runtime.players(), sent::add, proxyAuthenticator)) {
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(firstVote, firstPlayer, false)));
service.receive(fromProxy(vote(firstVote, firstPlayer, false)));
try (NeoForgeDeferredVoteStore.Claim claim = bounded.claim(firstPlayer, firstVote).orElseThrow()) {
assertEquals(NeoForgeDeferredVoteStore.CompletionResult.COMPLETED, claim.complete());
}
@@ -266,8 +256,8 @@ class NeoForgeProxySocketServiceTest {
sent.clear();
JsonEnvelope release = VotingPluginWire.voteDeliveryReceiptRelease(
"neoforge", firstVote, VotingPluginWire.SUB_VOTE, firstPlayer.toString());
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(release));
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(release));
service.receive(fromProxy(release));
service.receive(fromProxy(release));
assertEquals(2, sent.stream().filter(message ->
VotingPluginWire.SUB_VOTE_DELIVERY_RECEIPT_RELEASE_ACK.equals(message.getSubChannel()))
.count());
@@ -276,7 +266,7 @@ class NeoForgeProxySocketServiceTest {
}
}
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
assertEquals(NeoForgeDeferredVoteStore.OccurrenceState.COMPLETED,
runtime.deferredVotes().state(firstPlayer, firstVote));
assertEquals(NeoForgeVoteResult.Status.ALREADY_COMPLETED,
@@ -290,15 +280,15 @@ class NeoForgeProxySocketServiceTest {
void expiredReleaseRetryIsRenewedAndAcknowledged() throws IOException {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.storage().user(playerId).write(com.bencodez.advancedcore.api.user.UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.COMPLETED_DEFERRED_VOTES,
NeoForgeDeferredVoteStore.COMPLETED_DEFERRED_VOTES,
new com.bencodez.simpleapi.sql.data.DataValueString("v2|" + voteId + "|"
+ (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(8))));
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(VotingPluginWire.voteDeliveryReceiptRelease(
service.receive(fromProxy(VotingPluginWire.voteDeliveryReceiptRelease(
"neoforge", voteId, VotingPluginWire.SUB_VOTE, playerId.toString())));
assertEquals(VotingPluginWire.SUB_VOTE_DELIVERY_RECEIPT_RELEASE_ACK,
only(sent).getSubChannel());
@@ -311,32 +301,32 @@ class NeoForgeProxySocketServiceTest {
UUID playerId = UUID.randomUUID();
UUID firstVote = UUID.randomUUID();
UUID secondVote = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
runtime.rewardReplay().ifPresent(replay -> NeoForgeRewardReplayServiceAccess.stopAdmission(replay));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new com.bencodez.votingplugin.core.vote.SharedVoteIdentity(
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.rewardReplay().ifPresent(NeoForgeRewardReplayService::stopAdmission);
runtime.players().joined(new com.bencodez.votingplugin.core.vote.SharedVoteIdentity(
playerId, "Alex", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(
runtime.storage(), 2, 2, 2, 2, 1);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), java.time.Clock.systemUTC());
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = NeoForgeProxySocketServiceAccess.create(
new NeoForgeProxySocketConfigurationAccess(true, "neoforge", "proxy1", "127.0.0.1", 1297,
try (NeoForgeProxySocketService service = new NeoForgeProxySocketService(
new NeoForgeProxySocketConfiguration(true, "neoforge", "proxy1", "127.0.0.1", 1297,
"127.0.0.1", 1298, "socket-auth.key", false, false),
processor, runtime.players(), sent::add, proxyAuthenticator)) {
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(firstVote, playerId, false)));
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(secondVote, playerId, false)));
service.receive(fromProxy(vote(firstVote, playerId, false)));
service.receive(fromProxy(vote(secondVote, playerId, false)));
try (NeoForgeDeferredVoteStore.Claim claim = bounded.claim(playerId, firstVote).orElseThrow()) {
assertEquals(NeoForgeDeferredVoteStore.CompletionResult.COMPLETED, claim.complete());
}
try (NeoForgeDeferredVoteStore.Claim claim = bounded.claim(playerId, secondVote).orElseThrow()) {
assertEquals(NeoForgeDeferredVoteStore.CompletionResult.COMPLETED, claim.complete());
}
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(VotingPluginWire.voteDeliveryReceiptRelease(
service.receive(fromProxy(VotingPluginWire.voteDeliveryReceiptRelease(
"neoforge", firstVote, VotingPluginWire.SUB_VOTE, playerId.toString())));
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(VotingPluginWire.voteDeliveryReceiptRelease(
service.receive(fromProxy(VotingPluginWire.voteDeliveryReceiptRelease(
"neoforge", secondVote, VotingPluginWire.SUB_VOTE, playerId.toString())));
assertTrue(sent.isEmpty());
@@ -355,7 +345,7 @@ class NeoForgeProxySocketServiceTest {
doThrow(new IllegalStateException("listener failed")).when(listener).closeConnection();
assertThrows(IllegalStateException.class,
() -> NeoForgeProxySocketServiceAccess.closeTransport(listener, client));
() -> NeoForgeProxySocketService.closeTransport(listener, client));
verify(server).close();
verify(client).stopConnection();
@@ -364,18 +354,18 @@ class NeoForgeProxySocketServiceTest {
@Test
void malformedAndUnsupportedManagedTotalsVotesAreNotAcknowledgedOrStored() throws IOException {
UUID playerId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(UUID.randomUUID(), playerId, true)));
service.receive(fromProxy(vote(UUID.randomUUID(), playerId, true)));
JsonEnvelope malformed = JsonEnvelope.builder(VotingPluginWire.SUB_VOTE)
.schema(VotingPluginWire.SCHEMA_VERSION)
.put(VotingPluginWire.K_VOTE_ID, UUID.randomUUID().toString())
.put(VotingPluginWire.K_UUID, "not-a-uuid")
.put(VotingPluginWire.K_PLAYER, "Alex")
.put(VotingPluginWire.K_SERVICE, "Service").build();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(VotingPluginWire.requestVoteDeliveryAcknowledgement(malformed)));
service.receive(fromProxy(VotingPluginWire.requestVoteDeliveryAcknowledgement(malformed)));
assertTrue(sent.isEmpty());
assertTrue(runtime.deferredVotes().pending(playerId).isEmpty());
}
@@ -386,12 +376,12 @@ class NeoForgeProxySocketServiceTest {
void unsignedOrWrongProxyIdentityCannotAdmitAVote() throws IOException {
UUID playerId = UUID.randomUUID();
JsonEnvelope vote = vote(UUID.randomUUID(), playerId, false);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, vote);
NeoForgeProxySocketServiceAccess.receive(service, proxyAuthenticator.sign(vote, Domain.SOCKET_PROXY_BACKEND,
service.receive(vote);
service.receive(proxyAuthenticator.sign(vote, Domain.SOCKET_PROXY_BACKEND,
"other-proxy", "neoforge"));
assertTrue(runtime.deferredVotes().pending(playerId).isEmpty());
assertTrue(sent.isEmpty());
@@ -405,10 +395,10 @@ class NeoForgeProxySocketServiceTest {
UUID voteId = UUID.randomUUID();
TransportEnvelopeEncryption encryption = TransportEnvelopeEncryption.load(
directory.resolve("secretkey.key"), TransportEnvelopeEncryption.Domain.PROXY_BACKEND, true);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService active = NeoForgeProxySocketServiceAccess.create(
new NeoForgeProxySocketConfigurationAccess(true, "neoforge", "proxy1", "127.0.0.1", 1297,
try (NeoForgeProxySocketService active = new NeoForgeProxySocketService(
new NeoForgeProxySocketConfiguration(true, "neoforge", "proxy1", "127.0.0.1", 1297,
"127.0.0.1", 1298, "socket-auth.key", true, false),
runtime.voteProcessor(), runtime.players(), sent::add, proxyAuthenticator,
encryption, () -> { }, false)) {
@@ -416,7 +406,7 @@ class NeoForgeProxySocketServiceTest {
JsonEnvelope incoming = proxyAuthenticator.sign(
encryption.encrypt(vote(voteId, playerId, false)),
Domain.SOCKET_PROXY_BACKEND, "proxy1", "neoforge");
NeoForgeProxySocketServiceAccess.receive(active, incoming);
active.receive(incoming);
assertEquals(1, runtime.deferredVotes().pending(playerId).size());
SharedTransportEnvelopeAuthenticator.Verification verified = proxyAuthenticator.verify(
only(sent), Domain.SOCKET_PROXY_BACKEND, "proxy1");
@@ -431,16 +421,16 @@ class NeoForgeProxySocketServiceTest {
@Test
void capacityFailureIsNotAcknowledged() throws IOException {
UUID playerId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
for (int index = 0; index < NeoForgeDeferredVoteStoreAccess.MAX_DEFERRED_PER_USER; index++) {
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(UUID.randomUUID(), playerId, false)));
for (int index = 0; index < NeoForgeDeferredVoteStore.MAX_DEFERRED_PER_USER; index++) {
service.receive(fromProxy(vote(UUID.randomUUID(), playerId, false)));
}
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(vote(UUID.randomUUID(), playerId, false)));
service.receive(fromProxy(vote(UUID.randomUUID(), playerId, false)));
assertTrue(sent.isEmpty());
assertEquals(NeoForgeDeferredVoteStoreAccess.MAX_DEFERRED_PER_USER,
assertEquals(NeoForgeDeferredVoteStore.MAX_DEFERRED_PER_USER,
runtime.deferredVotes().pending(playerId).size());
}
}
@@ -450,11 +440,11 @@ class NeoForgeProxySocketServiceTest {
void statusAndPresenceUseExistingCapabilityProtocol() throws IOException {
UUID requestId = UUID.randomUUID();
UUID playerId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = service(runtime, sent)) {
sent.clear();
NeoForgeProxySocketServiceAccess.receive(service, fromProxy(VotingPluginWire.status("neoforge", requestId)));
service.receive(fromProxy(VotingPluginWire.status("neoforge", requestId)));
assertEquals(VotingPluginWire.SUB_STATUS_OKAY, only(sent).getSubChannel());
assertEquals(requestId.toString(), only(sent).getFields().get(VotingPluginWire.K_REQUEST_ID));
assertFalse(VotingPluginWire.advertisesVoteDeliveryAcknowledgement(only(sent)));
@@ -462,7 +452,7 @@ class NeoForgeProxySocketServiceTest {
assertFalse(VotingPluginWire.advertisesVoteDelayRejectionAcknowledgement(only(sent)));
sent.clear();
var identity = new com.bencodez.votingplugin.core.vote.SharedVoteIdentity(playerId, "Alex", true);
NeoForgePlayerDirectoryAccess.joined(runtime.players(), identity);
runtime.players().joined(identity);
service.playerOnline(identity);
assertTrue(sent.stream().anyMatch(message -> VotingPluginWire.SUB_BACKEND_STARTED
.equals(message.getSubChannel())));
@@ -477,7 +467,7 @@ class NeoForgeProxySocketServiceTest {
@Test
void startupAndHeartbeatDoNotAdvertiseUnsupportedDelayRejectionAcknowledgements() throws IOException {
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService ignored = service(runtime, sent)) {
List<JsonEnvelope> capabilities = sent.stream().filter(message ->
@@ -496,7 +486,7 @@ class NeoForgeProxySocketServiceTest {
@Test
void existingBungeeSettingsShapeIsValidated() throws IOException {
ConfigurationNode disabled = load("UseBungeecord: false\n");
assertFalse(NeoForgeProxySocketConfigurationAccess.load(disabled).enabled());
assertFalse(NeoForgeProxySocketConfiguration.load(disabled).enabled());
ConfigurationNode enabled = load("""
UseBungeecord: true
BungeeMethod: SOCKETS
@@ -510,11 +500,11 @@ class NeoForgeProxySocketServiceTest {
Port: 1298
SocketAuthenticationKeyFile: socket-auth.key
""");
NeoForgeProxySocketConfigurationAccess parsed = NeoForgeProxySocketConfigurationAccess.load(enabled);
NeoForgeProxySocketConfiguration parsed = NeoForgeProxySocketConfiguration.load(enabled);
assertTrue(parsed.enabled());
assertEquals("neoforge", parsed.server());
assertEquals("proxy1", parsed.proxyName());
assertThrows(IllegalArgumentException.class, () -> NeoForgeProxySocketConfigurationAccess.load(load("""
assertThrows(IllegalArgumentException.class, () -> NeoForgeProxySocketConfiguration.load(load("""
UseBungeecord: true
BungeeMethod: SOCKETS
Server: PleaseSet
@@ -525,11 +515,11 @@ class NeoForgeProxySocketServiceTest {
@Test
void stalledSocketSendDoesNotBlockLifecycleEventsAndShutdownIsBounded() throws Exception {
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
NeoForgeProxySocketService service = NeoForgeProxySocketServiceAccess.create(
new NeoForgeProxySocketConfigurationAccess(true, "neoforge", "proxy1", "127.0.0.1", 1297,
NeoForgeProxySocketService service = new NeoForgeProxySocketService(
new NeoForgeProxySocketConfiguration(true, "neoforge", "proxy1", "127.0.0.1", 1297,
"127.0.0.1", 1298, "socket-auth.key", false, false),
runtime.voteProcessor(), runtime.players(), ignored -> {
entered.countDown();
@@ -557,13 +547,13 @@ class NeoForgeProxySocketServiceTest {
@Test
void rejectedPresenceSendTriggersAuthoritativeRecoveryAfterQueueDrains() throws Exception {
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch recovered = new CountDownLatch(1);
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
NeoForgeProxySocketService service = NeoForgeProxySocketServiceAccess.create(
new NeoForgeProxySocketConfigurationAccess(true, "neoforge", "proxy1", "127.0.0.1", 1297,
NeoForgeProxySocketService service = new NeoForgeProxySocketService(
new NeoForgeProxySocketConfiguration(true, "neoforge", "proxy1", "127.0.0.1", 1297,
"127.0.0.1", 1298, "socket-auth.key", false, false),
runtime.voteProcessor(), runtime.players(), envelope -> {
sent.add(envelope);
@@ -598,14 +588,14 @@ class NeoForgeProxySocketServiceTest {
@Test
void snapshotAndLogoutArePublishedInOnePresenceOrder() throws Exception {
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
CountDownLatch snapshotEntered = new CountDownLatch(1);
CountDownLatch releaseSnapshot = new CountDownLatch(1);
CountDownLatch logoutFinished = new CountDownLatch(1);
AtomicBoolean blockSnapshot = new AtomicBoolean();
CopyOnWriteArrayList<JsonEnvelope> sent = new CopyOnWriteArrayList<>();
try (NeoForgeProxySocketService service = NeoForgeProxySocketServiceAccess.create(
new NeoForgeProxySocketConfigurationAccess(true, "neoforge", "proxy1", "127.0.0.1", 1297,
try (NeoForgeProxySocketService service = new NeoForgeProxySocketService(
new NeoForgeProxySocketConfiguration(true, "neoforge", "proxy1", "127.0.0.1", 1297,
"127.0.0.1", 1298, "socket-auth.key", false, false),
runtime.voteProcessor(), runtime.players(), envelope -> {
sent.add(envelope);
@@ -628,7 +618,7 @@ class NeoForgeProxySocketServiceTest {
service.playerOnline(identity);
sent.clear();
blockSnapshot.set(true);
Thread snapshot = new Thread(() -> NeoForgeProxySocketServiceAccess.receive(service, fromProxy(
Thread snapshot = new Thread(() -> service.receive(fromProxy(
VotingPluginWire.presenceSnapshotRequest("neoforge", UUID.randomUUID(), incarnation,
startedAt, System.currentTimeMillis()))));
snapshot.start();
@@ -659,7 +649,7 @@ class NeoForgeProxySocketServiceTest {
}
private NeoForgeProxySocketService service(NeoForgeRuntime runtime, List<JsonEnvelope> sent) {
return NeoForgeProxySocketServiceAccess.create(new NeoForgeProxySocketConfigurationAccess(true,
return new NeoForgeProxySocketService(new NeoForgeProxySocketConfiguration(true,
"neoforge", "proxy1", "127.0.0.1", 1297, "127.0.0.1", 1298,
"socket-auth.key", false, false),
runtime.voteProcessor(), runtime.players(), sent::add, proxyAuthenticator);
@@ -1,21 +1,4 @@
package com.bencodez.votingplugin.tests.neoforge;
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
import com.bencodez.votingplugin.neoforge.NeoForgeDeferredVote;
import com.bencodez.votingplugin.neoforge.NeoForgeDeferredVoteStore;
import com.bencodez.votingplugin.tests.neoforge.NeoForgeNativeRewardActionsAccess;
import com.bencodez.votingplugin.neoforge.NeoForgePlayerDirectory;
import com.bencodez.votingplugin.tests.neoforge.NeoForgeRewardActionsAccess;
import com.bencodez.votingplugin.tests.neoforge.NeoForgeRewardConfigurationAccess;
import com.bencodez.votingplugin.neoforge.NeoForgeRewardPlan;
import com.bencodez.votingplugin.neoforge.NeoForgeRewardReplayService;
import com.bencodez.votingplugin.neoforge.NeoForgeRuntime;
import com.bencodez.votingplugin.neoforge.NeoForgeServerScheduler;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteAccount;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteProcessor;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteRequest;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteResult;
package com.bencodez.votingplugin.neoforge;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -50,7 +33,7 @@ class NeoForgeRewardReplayServiceTest {
@Test
void deferredIndexesAreCreatedByReplayWorkerBeforeDiscovery() throws Exception {
writeConfiguration(false);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, new RecordingActions())) {
assertFalse(indexExists("VotingPlugin_NeoForgeDeferredPending"));
assertFalse(indexExists("VotingPlugin_NeoForgeDeferredCompleted"));
@@ -82,14 +65,14 @@ class NeoForgeRewardReplayServiceTest {
logger.addHandler(handler);
UUID healthyPlayer = UUID.randomUUID();
UUID healthyVote = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
runtime.rewardReplay().ifPresent(replay -> NeoForgeRewardReplayServiceAccess.stopAdmission(replay));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(healthyPlayer, "Healthy", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.rewardReplay().ifPresent(NeoForgeRewardReplayService::stopAdmission);
runtime.players().joined(new SharedVoteIdentity(healthyPlayer, "Healthy", true));
retain(runtime, healthyVote, healthyPlayer, "Service");
try (var connection = DriverManager.getConnection("jdbc:sqlite:"
+ directory.resolve("VotingPlugin.db").toAbsolutePath());
var insert = connection.prepareStatement(
"INSERT INTO `" + NeoForgeRuntimeAccess.USER_TABLE_NAME
"INSERT INTO `" + NeoForgeRuntime.USER_TABLE_NAME
+ "` (`UUID`, `DeferredVotes`) VALUES (?, ?)")) {
insert.setString(1, "not-a-uuid");
insert.setString(2, "malformed-but-relevant");
@@ -97,8 +80,8 @@ class NeoForgeRewardReplayServiceTest {
}
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
runtime.rewardReplay().ifPresent(replay -> NeoForgeRewardReplayServiceAccess.stopAdmission(replay));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.rewardReplay().ifPresent(NeoForgeRewardReplayService::stopAdmission);
assertEquals(List.of(healthyPlayer), runtime.deferredVotes().users());
assertTrue(reported.await(5, TimeUnit.SECONDS));
assertEquals(1, runtime.deferredVotes().pending(healthyPlayer).size());
@@ -125,10 +108,10 @@ class NeoForgeRewardReplayServiceTest {
java.util.logging.Logger logger = java.util.logging.Logger.getLogger(
NeoForgeRewardReplayService.class.getName());
logger.addHandler(handler);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, new RecordingActions())) {
runtime.storage().user(playerId).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES,
NeoForgeDeferredVoteStore.DEFERRED_VOTES,
new DataValueString("malformed-but-relevant"));
assertTrue(replay.replayOnce().get(5, TimeUnit.SECONDS).isEmpty());
@@ -137,7 +120,7 @@ class NeoForgeRewardReplayServiceTest {
assertEquals(1, reports.get());
assertEquals("malformed-but-relevant", runtime.storage().user(playerId)
.readRow(UserStorage.SQLITE).stream()
.filter(column -> column.getName().equalsIgnoreCase(NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES))
.filter(column -> column.getName().equalsIgnoreCase(NeoForgeDeferredVoteStore.DEFERRED_VOTES))
.findFirst().orElseThrow().getValue().getString());
} finally {
logger.removeHandler(handler);
@@ -158,17 +141,17 @@ class NeoForgeRewardReplayServiceTest {
java.util.logging.Logger logger = java.util.logging.Logger.getLogger(
NeoForgeRewardReplayService.class.getName());
logger.addHandler(handler);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
runtime.rewardReplay().ifPresent(replay -> NeoForgeRewardReplayServiceAccess.stopAdmission(replay));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.rewardReplay().ifPresent(NeoForgeRewardReplayService::stopAdmission);
UUID completedPlayer = UUID.randomUUID();
UUID pendingPlayer = UUID.randomUUID();
UUID completedVote = UUID.randomUUID();
UUID pendingVote = UUID.randomUUID();
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(completedPlayer, "Completed", true));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(pendingPlayer, "Pending", true));
NeoForgeDeferredVoteStore bounded = NeoForgeDeferredVoteStoreAccess.create(
runtime.players().joined(new SharedVoteIdentity(completedPlayer, "Completed", true));
runtime.players().joined(new SharedVoteIdentity(pendingPlayer, "Pending", true));
NeoForgeDeferredVoteStore bounded = new NeoForgeDeferredVoteStore(
runtime.storage(), 2, 2, 1, 1);
NeoForgeVoteProcessor processor = NeoForgeVoteProcessorAccess.create(runtime.voteConfiguration(),
NeoForgeVoteProcessor processor = new NeoForgeVoteProcessor(runtime.voteConfiguration(),
runtime.accounting(), bounded, runtime.players(), java.time.Clock.systemUTC());
assertEquals(NeoForgeVoteResult.Status.DEFERRED, processor.process(new NeoForgeVoteRequest(
completedVote, completedPlayer, "Completed", "Service", 100L,
@@ -183,9 +166,9 @@ class NeoForgeRewardReplayServiceTest {
RecordingActions actions = new RecordingActions();
actions.scheduler = runtime.scheduler();
try (NeoForgeRewardReplayService replay = NeoForgeRewardReplayServiceAccess.create(
try (NeoForgeRewardReplayService replay = new NeoForgeRewardReplayService(
runtime.voteConfiguration(),
new NeoForgeRewardConfigurationAccess(runtime.config(), runtime.voteSites(), runtime.specialRewards()),
new NeoForgeRewardConfiguration(runtime.config(), runtime.voteSites(), runtime.specialRewards()),
runtime.accounting(), bounded, runtime.players(), actions)) {
List<NeoForgeRewardReplayService.ReplayResult> result = replay.replayOnce()
.get(5, TimeUnit.SECONDS);
@@ -204,10 +187,10 @@ class NeoForgeRewardReplayServiceTest {
@Test
void replayDeadlinesHandleMissingQuarantineAndNanoTimeWrap() {
assertTrue(NeoForgeRewardReplayServiceAccess.deadlineReached(null, -10L));
assertFalse(NeoForgeRewardReplayServiceAccess.deadlineReached(Long.MAX_VALUE, Long.MAX_VALUE));
assertFalse(NeoForgeRewardReplayServiceAccess.deadlineReached(Long.MIN_VALUE + 5L, Long.MAX_VALUE - 5L));
assertTrue(NeoForgeRewardReplayServiceAccess.deadlineReached(Long.MAX_VALUE - 5L, Long.MIN_VALUE + 5L));
assertTrue(NeoForgeRewardReplayService.deadlineReached(null, -10L));
assertFalse(NeoForgeRewardReplayService.deadlineReached(Long.MAX_VALUE, Long.MAX_VALUE));
assertFalse(NeoForgeRewardReplayService.deadlineReached(Long.MIN_VALUE + 5L, Long.MAX_VALUE - 5L));
assertTrue(NeoForgeRewardReplayService.deadlineReached(Long.MAX_VALUE - 5L, Long.MIN_VALUE + 5L));
}
@Test
@@ -217,9 +200,9 @@ class NeoForgeRewardReplayServiceTest {
.replace("ProcessRewards: true", "ProcessRewards: false"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
List<NeoForgeRewardReplayService.ReplayResult> result = replay.replayOnce().get(5, TimeUnit.SECONDS);
assertEquals(NeoForgeRewardReplayService.Status.BLOCKED_UNSUPPORTED, result.get(0).status());
@@ -234,11 +217,11 @@ class NeoForgeRewardReplayServiceTest {
writeConfiguration(false);
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "OldName", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "OldName", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "NewName", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "NewName", true));
assertEquals(NeoForgeRewardReplayService.Status.COMPLETED,
runOne(runtime, replay, actions).status());
assertEquals(List.of("say NewName"), actions.rendered);
@@ -253,12 +236,12 @@ class NeoForgeRewardReplayServiceTest {
.replaceFirst("(?m)^(\\s*)WaitUntilVoteDelay: false", "$0\n$1ForceOffline: true"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "OldName", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "OldName", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
NeoForgeRuntimeTest.FakePlayer renamed = new NeoForgeRuntimeTest.FakePlayer(playerId, "NewName");
NeoForgePlayerDirectoryAccess.joined(runtime.players(), renamed);
runtime.players().joined(renamed);
runtime.players().left(renamed);
assertEquals(NeoForgeRewardReplayService.Status.COMPLETED,
@@ -275,19 +258,19 @@ class NeoForgeRewardReplayServiceTest {
.replaceFirst("(?m)^(\\s*)WaitUntilVoteDelay: false", "$0\n$1ForceOffline: true"));
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, new RecordingActions())) {
SharedVoteIdentity oldIdentity = new SharedVoteIdentity(playerId, "OldName", true);
NeoForgePlayerDirectoryAccess.joined(runtime.players(), oldIdentity);
NeoForgeRewardReplayServiceAccess.rememberIdentity(replay, oldIdentity);
runtime.players().joined(oldIdentity);
replay.rememberIdentity(oldIdentity);
retain(runtime, voteId, playerId, "Service");
NeoForgeRuntimeTest.FakePlayer renamed = new NeoForgeRuntimeTest.FakePlayer(playerId, "NewName");
NeoForgeRewardReplayServiceAccess.rememberIdentity(replay, NeoForgePlayerDirectoryAccess.joinedIdentity(runtime.players(), renamed));
replay.rememberIdentity(runtime.players().joinedIdentity(renamed));
runtime.players().left(renamed);
}
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
assertEquals(NeoForgeRewardReplayService.Status.COMPLETED,
runOne(runtime, replay, actions).status());
@@ -303,10 +286,10 @@ class NeoForgeRewardReplayServiceTest {
.replace("AddTotalsOffline: true", "AddTotalsOffline: false"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
retain(runtime, UUID.randomUUID(), playerId, "Service");
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
assertEquals(NeoForgeRewardReplayService.Status.COMPLETED,
runOne(runtime, replay, actions).status());
@@ -321,11 +304,11 @@ class NeoForgeRewardReplayServiceTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
runtime.storage().user(playerId).write(UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES,
NeoForgeDeferredVoteStore.DEFERRED_VOTES,
new DataValueString("v1|" + voteId
+ "|QWxleA|U2VydmljZQ|U3VwcG9ydGVk|100|true|true|true"));
@@ -344,9 +327,9 @@ class NeoForgeRewardReplayServiceTest {
.replace("CloseInventoryOnVote: false", "CloseInventoryOnVote: true"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
List<NeoForgeRewardReplayService.ReplayResult> result = replay.replayOnce().get(5, TimeUnit.SECONDS);
@@ -365,7 +348,7 @@ class NeoForgeRewardReplayServiceTest {
.replace("LimitVotePoints: -1", "LimitVotePoints: 4"));
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteResult retained = runtime.voteProcessor().process(new NeoForgeVoteRequest(voteId, playerId,
"Alex", "Service", 100L, false, true, false, NeoForgeVoteRequest.Scope.COMPLETE));
assertEquals(NeoForgeVoteResult.Status.DEFERRED, retained.status());
@@ -378,9 +361,9 @@ class NeoForgeRewardReplayServiceTest {
.replace("LimitVotePoints: 4", "LimitVotePoints: 1"));
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
assertEquals(NeoForgeRewardReplayService.Status.COMPLETED,
runOne(runtime, replay, actions).status());
NeoForgeVoteAccount account = runtime.accounting().load(playerId).orElseThrow();
@@ -398,16 +381,16 @@ class NeoForgeRewardReplayServiceTest {
.replace("'say %player%'", "'say %ServiceSite%'"));
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, voteId, playerId, "Supported");
}
Files.writeString(directory.resolve("VoteSites.yml"), Files.readString(directory.resolve("VoteSites.yml"))
.replace("ServiceSite: Service", "ServiceSite: RenamedService"));
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
assertEquals(NeoForgeRewardReplayService.Status.COMPLETED,
runOne(runtime, replay, actions).status());
@@ -420,16 +403,16 @@ class NeoForgeRewardReplayServiceTest {
writeConfiguration(false);
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, voteId, playerId, "Service");
}
Files.writeString(directory.resolve("VoteSites.yml"), Files.readString(directory.resolve("VoteSites.yml"))
.replace(" Supported:", " supported:"));
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
assertEquals(NeoForgeRewardReplayService.Status.COMPLETED,
runOne(runtime, replay, actions).status());
@@ -447,7 +430,7 @@ class NeoForgeRewardReplayServiceTest {
Files.writeString(directory.resolve("VoteSites.yml"), updated);
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
retain(runtime, UUID.randomUUID(), playerId, "Service");
@@ -467,9 +450,9 @@ class NeoForgeRewardReplayServiceTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
RecordingActions strictActions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, strictActions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, voteId, playerId, "Service");
assertEquals(NeoForgeRewardReplayService.Status.BLOCKED_UNSUPPORTED,
replay.replayOnce().get(5, TimeUnit.SECONDS).get(0).status());
@@ -478,9 +461,9 @@ class NeoForgeRewardReplayServiceTest {
Files.writeString(directory.resolve("Config.yml"), Files.readString(directory.resolve("Config.yml"))
+ "CaseInsensitiveYMLFiles: true\n");
RecordingActions insensitiveActions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, insensitiveActions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
assertEquals(NeoForgeRewardReplayService.Status.COMPLETED,
runOne(runtime, replay, insensitiveActions).status());
}
@@ -490,7 +473,7 @@ class NeoForgeRewardReplayServiceTest {
void unjoinedCommandPlaceholderNameMustUseMinecraftSyntax() throws Exception {
writeConfiguration(false);
UUID playerId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteResult result = runtime.voteProcessor().process(new NeoForgeVoteRequest(
UUID.randomUUID(), playerId, "Alex;op", "Service", 100L, true, true, false,
NeoForgeVoteRequest.Scope.COMPLETE));
@@ -505,12 +488,12 @@ class NeoForgeRewardReplayServiceTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.storage().user(playerId).write(com.bencodez.advancedcore.api.user.UserStorage.SQLITE,
NeoForgeDeferredVoteStoreAccess.DEFERRED_VOTES, new com.bencodez.simpleapi.sql.data.DataValueString(
NeoForgeDeferredVoteStore.DEFERRED_VOTES, new com.bencodez.simpleapi.sql.data.DataValueString(
"v1|" + voteId + "|QGE|U2VydmljZQ|U3VwcG9ydGVk|100|true|true|false"));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
List<NeoForgeRewardReplayService.ReplayResult> results = replay.replayOnce().get(5, TimeUnit.SECONDS);
assertEquals(NeoForgeRewardReplayService.Status.BLOCKED_UNSUPPORTED, results.get(0).status());
@@ -524,8 +507,8 @@ class NeoForgeRewardReplayServiceTest {
writeConfiguration(false);
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
actions.throwSynchronously = true;
try (NeoForgeRewardReplayService replay = service(runtime, actions)) {
@@ -554,9 +537,9 @@ class NeoForgeRewardReplayServiceTest {
""");
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
assertEquals(NeoForgeRewardReplayService.Status.BLOCKED_UNSUPPORTED,
replay.replayOnce().get(5, TimeUnit.SECONDS).get(0).status());
@@ -570,13 +553,13 @@ class NeoForgeRewardReplayServiceTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, voteId, playerId, "Service");
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
NeoForgeRewardReplayService.ReplayResult result = runOne(runtime, replay, actions);
assertEquals(NeoForgeRewardReplayService.Status.COMPLETED, result.status());
assertEquals(List.of("say Alex"), actions.rendered);
@@ -587,7 +570,7 @@ class NeoForgeRewardReplayServiceTest {
assertTrue(replay.replayOnce().get(5, TimeUnit.SECONDS).isEmpty());
assertEquals(1, actions.calls.get());
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
assertEquals(1, runtime.accounting().load(playerId).orElseThrow().allTimeTotal());
assertEquals(NeoForgeDeferredVoteStore.OccurrenceState.COMPLETED,
runtime.deferredVotes().state(playerId, voteId));
@@ -600,16 +583,16 @@ class NeoForgeRewardReplayServiceTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
AtomicInteger externalEffects = new AtomicInteger();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, voteId, playerId, "Service");
NeoForgeRewardActionsAccess actions = (vote, plan) -> {
NeoForgeRewardActions actions = (vote, plan) -> {
externalEffects.incrementAndGet();
runtime.storage().close();
return CompletableFuture.completedFuture(null);
};
try (NeoForgeRewardReplayService replay = NeoForgeRewardReplayServiceAccess.create(runtime.voteConfiguration(),
new NeoForgeRewardConfigurationAccess(runtime.config(), runtime.voteSites(), runtime.specialRewards()),
try (NeoForgeRewardReplayService replay = new NeoForgeRewardReplayService(runtime.voteConfiguration(),
new NeoForgeRewardConfiguration(runtime.config(), runtime.voteSites(), runtime.specialRewards()),
runtime.accounting(), runtime.deferredVotes(), runtime.players(), actions)) {
assertEquals(NeoForgeRewardReplayService.Status.COMPLETION_UNCERTAIN,
replay.replayOnce().get(5, TimeUnit.SECONDS).get(0).status());
@@ -618,9 +601,9 @@ class NeoForgeRewardReplayServiceTest {
assertEquals(1, externalEffects.get());
RecordingActions retry = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, retry)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
assertEquals(NeoForgeRewardReplayService.Status.REWARD_UNCERTAIN,
replay.replayOnce().get(5, TimeUnit.SECONDS).get(0).status());
assertEquals(0, retry.calls.get());
@@ -634,18 +617,18 @@ class NeoForgeRewardReplayServiceTest {
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
AtomicInteger effects = new AtomicInteger();
NeoForgeRewardActionsAccess actions = (vote, plan) -> {
NeoForgeRewardActions actions = (vote, plan) -> {
effects.incrementAndGet();
return CompletableFuture.completedFuture(null);
};
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, voteId, playerId, "Service");
try (var connection = DriverManager.getConnection("jdbc:sqlite:"
+ directory.resolve("VotingPlugin.db").toAbsolutePath());
var statement = connection.createStatement()) {
statement.execute("CREATE TRIGGER fail_reward_fence BEFORE UPDATE OF DeferredVotes ON `"
+ NeoForgeRuntimeAccess.USER_TABLE_NAME
+ NeoForgeRuntime.USER_TABLE_NAME
+ "` BEGIN SELECT RAISE(FAIL, 'controlled write failure'); END");
try (NeoForgeRewardReplayService replay = service(runtime, actions)) {
assertEquals(NeoForgeRewardReplayService.Status.REWARD_FAILED,
@@ -670,21 +653,21 @@ class NeoForgeRewardReplayServiceTest {
writeConfiguration(false);
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, voteId, playerId, "Service");
try (var claim = runtime.deferredVotes().claim(playerId, voteId).orElseThrow()) {
assertEquals(NeoForgeDeferredVoteStoreAccess.FenceResult.FENCED, NeoForgeDeferredVoteStoreAccess.fenceExternalEffects(claim));
assertEquals(NeoForgeDeferredVoteStore.FenceResult.FENCED, claim.fenceExternalEffects());
}
}
AtomicInteger effects = new AtomicInteger();
NeoForgeRewardActionsAccess actions = (vote, plan) -> {
NeoForgeRewardActions actions = (vote, plan) -> {
effects.incrementAndGet();
return CompletableFuture.completedFuture(null);
};
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
assertEquals(NeoForgeRewardReplayService.Status.REWARD_UNCERTAIN,
replay.replayOnce().get(5, TimeUnit.SECONDS).get(0).status());
assertEquals(0, effects.get());
@@ -698,8 +681,8 @@ class NeoForgeRewardReplayServiceTest {
writeConfiguration(false);
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, voteId, playerId, "Service");
RecordingActions failing = new RecordingActions();
failing.fail = true;
@@ -726,10 +709,10 @@ class NeoForgeRewardReplayServiceTest {
UUID offlinePlayer = UUID.randomUUID();
UUID healthyPlayer = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(blockedPlayer, "Blocked", true));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(healthyPlayer, "Healthy", true));
runtime.players().joined(new SharedVoteIdentity(blockedPlayer, "Blocked", true));
runtime.players().joined(new SharedVoteIdentity(healthyPlayer, "Healthy", true));
retain(runtime, UUID.randomUUID(), blockedPlayer, "Unsupported");
retain(runtime, UUID.randomUUID(), offlinePlayer, "Service");
retain(runtime, UUID.randomUUID(), healthyPlayer, "Service");
@@ -770,9 +753,9 @@ class NeoForgeRewardReplayServiceTest {
""");
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
List<NeoForgeRewardReplayService.ReplayResult> results = replay.replayOnce().get(5, TimeUnit.SECONDS);
assertEquals(NeoForgeRewardReplayService.Status.BLOCKED_UNSUPPORTED, results.get(0).status());
@@ -802,9 +785,9 @@ class NeoForgeRewardReplayServiceTest {
""");
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
List<NeoForgeRewardReplayService.ReplayResult> result = replay.replayOnce().get(5, TimeUnit.SECONDS);
assertEquals(NeoForgeRewardReplayService.Status.BLOCKED_UNSUPPORTED, result.get(0).status());
@@ -822,9 +805,9 @@ class NeoForgeRewardReplayServiceTest {
"- named-reward"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
List<NeoForgeRewardReplayService.ReplayResult> result = replay.replayOnce().get(5, TimeUnit.SECONDS);
assertEquals(NeoForgeRewardReplayService.Status.BLOCKED_UNSUPPORTED, result.get(0).status());
@@ -842,9 +825,9 @@ class NeoForgeRewardReplayServiceTest {
"Messages:\n Player: 'Thanks %player% from %ServiceSite% at %SiteName%'"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
CompletableFuture<List<NeoForgeRewardReplayService.ReplayResult>> completion = replay.replayOnce();
assertTrue(actions.scheduled.await(5, TimeUnit.SECONDS));
@@ -865,9 +848,9 @@ class NeoForgeRewardReplayServiceTest {
"Messages:\n Player: 'Thanks %player% from %SiteName%'"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
List<NeoForgeRewardReplayService.ReplayResult> result = replay.replayOnce().get(5, TimeUnit.SECONDS);
@@ -888,9 +871,9 @@ class NeoForgeRewardReplayServiceTest {
"Messages:\n Player: 'Thanks %player% from %ServiceSite%'"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "&aService");
assertEquals(NeoForgeRewardReplayService.Status.COMPLETED,
@@ -910,9 +893,9 @@ class NeoForgeRewardReplayServiceTest {
"Messages:\n Player: 'Thanks %player% from %ServiceSite%'"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "%player%");
assertEquals(NeoForgeRewardReplayService.Status.COMPLETED,
@@ -933,9 +916,9 @@ class NeoForgeRewardReplayServiceTest {
"Messages:\n Player: 'Thanks %player% from %SiteName%'"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
assertEquals(NeoForgeRewardReplayService.Status.COMPLETED,
@@ -954,9 +937,9 @@ class NeoForgeRewardReplayServiceTest {
"Messages:\n Player: '[Text=\"Vote\",url=\"https://example.com\"]'"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
List<NeoForgeRewardReplayService.ReplayResult> result = replay.replayOnce().get(5, TimeUnit.SECONDS);
@@ -976,9 +959,9 @@ class NeoForgeRewardReplayServiceTest {
"Commands:\n - 'say %player%'\n Messages:\n Player: 'Thanks %player%'"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
List<NeoForgeRewardReplayService.ReplayResult> result = replay.replayOnce().get(5, TimeUnit.SECONDS);
assertEquals(NeoForgeRewardReplayService.Status.BLOCKED_UNSUPPORTED, result.get(0).status());
@@ -995,9 +978,9 @@ class NeoForgeRewardReplayServiceTest {
"- 'say %player%'", "- 'say first'\n - 'say second'"));
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
List<NeoForgeRewardReplayService.ReplayResult> result = replay.replayOnce().get(5, TimeUnit.SECONDS);
assertEquals(NeoForgeRewardReplayService.Status.BLOCKED_UNSUPPORTED, result.get(0).status());
@@ -1009,14 +992,14 @@ class NeoForgeRewardReplayServiceTest {
void disabledSiteAfterRetentionRemainsPending() throws Exception {
writeConfiguration(false);
UUID playerId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
}
String sites = Files.readString(directory.resolve("VoteSites.yml"));
Files.writeString(directory.resolve("VoteSites.yml"), sites.replaceFirst("Enabled: true", "Enabled: false"));
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
List<NeoForgeRewardReplayService.ReplayResult> result = replay.replayOnce().get(5, TimeUnit.SECONDS);
assertEquals(NeoForgeRewardReplayService.Status.BLOCKED_UNSUPPORTED, result.get(0).status());
@@ -1030,9 +1013,9 @@ class NeoForgeRewardReplayServiceTest {
writeConfiguration(true);
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Unsupported");
retain(runtime, UUID.randomUUID(), playerId, "Service");
@@ -1051,16 +1034,16 @@ class NeoForgeRewardReplayServiceTest {
writeConfiguration(true);
RecordingActions actions = new RecordingActions();
UUID healthy = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
for (int index = 0; index < NeoForgeRewardReplayServiceAccess.MAX_PER_RUN; index++) {
for (int index = 0; index < NeoForgeRewardReplayService.MAX_PER_RUN; index++) {
UUID blocked = UUID.randomUUID();
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(blocked, "Blocked" + index, true));
runtime.players().joined(new SharedVoteIdentity(blocked, "Blocked" + index, true));
retain(runtime, UUID.randomUUID(), blocked, "Unsupported");
}
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(healthy, "Healthy", true));
runtime.players().joined(new SharedVoteIdentity(healthy, "Healthy", true));
retain(runtime, UUID.randomUUID(), healthy, "Service");
assertEquals(NeoForgeRewardReplayServiceAccess.MAX_PER_RUN,
assertEquals(NeoForgeRewardReplayService.MAX_PER_RUN,
replay.replayOnce().get(5, TimeUnit.SECONDS).size());
CompletableFuture<List<NeoForgeRewardReplayService.ReplayResult>> second = replay.replayOnce();
@@ -1077,9 +1060,9 @@ class NeoForgeRewardReplayServiceTest {
writeConfiguration(false);
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
CompletableFuture<List<NeoForgeRewardReplayService.ReplayResult>> result = replay.replayOnce();
assertTrue(actions.scheduled.await(5, TimeUnit.SECONDS));
@@ -1097,9 +1080,9 @@ class NeoForgeRewardReplayServiceTest {
writeConfiguration(false);
UUID playerId = UUID.randomUUID();
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, UUID.randomUUID(), playerId, "Service");
CompletableFuture<List<NeoForgeRewardReplayService.ReplayResult>> result = replay.replayOnce();
assertTrue(actions.scheduled.await(5, TimeUnit.SECONDS));
@@ -1115,7 +1098,7 @@ class NeoForgeRewardReplayServiceTest {
void unsuccessfulNativeCommandDoesNotReportRewardSuccess() {
NeoForgeServerScheduler scheduler = new NeoForgeServerScheduler();
try {
NeoForgeNativeRewardActionsAccess actions = new NeoForgeNativeRewardActionsAccess(
NeoForgeNativeRewardActions actions = new NeoForgeNativeRewardActions(
new FailingCommandServer(), scheduler, new NeoForgePlayerDirectory());
NeoForgeDeferredVote vote = new NeoForgeDeferredVote(UUID.randomUUID(), UUID.randomUUID(),
"Alex", "Service", "Supported", 100L, true, true, true);
@@ -1136,7 +1119,7 @@ class NeoForgeRewardReplayServiceTest {
NeoForgeServerScheduler scheduler = new NeoForgeServerScheduler();
ThrowingCommandServer server = new ThrowingCommandServer();
try {
NeoForgeNativeRewardActionsAccess actions = new NeoForgeNativeRewardActionsAccess(
NeoForgeNativeRewardActions actions = new NeoForgeNativeRewardActions(
server, scheduler, new NeoForgePlayerDirectory());
NeoForgeDeferredVote vote = new NeoForgeDeferredVote(UUID.randomUUID(), UUID.randomUUID(),
"Alex", "Service", "Supported", 100L, true, true, true);
@@ -1150,7 +1133,7 @@ class NeoForgeRewardReplayServiceTest {
java.util.concurrent.ExecutionException failure = assertThrows(
java.util.concurrent.ExecutionException.class,
() -> completion.get(5, TimeUnit.SECONDS));
assertTrue(ReflectiveAccess.type("com.bencodez.votingplugin.neoforge.NeoForgeNativeRewardActions$UncertainRewardOutcomeException").isInstance(failure.getCause()));
assertTrue(failure.getCause() instanceof NeoForgeNativeRewardActions.UncertainRewardOutcomeException);
assertEquals(1, server.commands.dispatches.get());
} finally {
scheduler.close();
@@ -1162,7 +1145,7 @@ class NeoForgeRewardReplayServiceTest {
NeoForgeServerScheduler scheduler = new NeoForgeServerScheduler();
CallbackCommandServer server = new CallbackCommandServer(false, true);
try {
NeoForgeNativeRewardActionsAccess actions = new NeoForgeNativeRewardActionsAccess(
NeoForgeNativeRewardActions actions = new NeoForgeNativeRewardActions(
server, scheduler, new NeoForgePlayerDirectory());
NeoForgeDeferredVote vote = new NeoForgeDeferredVote(UUID.randomUUID(), UUID.randomUUID(),
"Alex", "Service", "Supported", 100L, true, true, true);
@@ -1176,7 +1159,7 @@ class NeoForgeRewardReplayServiceTest {
java.util.concurrent.ExecutionException failure = assertThrows(
java.util.concurrent.ExecutionException.class,
() -> completion.get(5, TimeUnit.SECONDS));
assertTrue(ReflectiveAccess.type("com.bencodez.votingplugin.neoforge.NeoForgeNativeRewardActions$UncertainRewardOutcomeException").isInstance(failure.getCause()));
assertTrue(failure.getCause() instanceof NeoForgeNativeRewardActions.UncertainRewardOutcomeException);
assertEquals(1, server.commands.dispatches.get());
} finally {
scheduler.close();
@@ -1188,7 +1171,7 @@ class NeoForgeRewardReplayServiceTest {
NeoForgeServerScheduler scheduler = new NeoForgeServerScheduler();
CallbackCommandServer server = new CallbackCommandServer(true, false);
try {
NeoForgeNativeRewardActionsAccess actions = new NeoForgeNativeRewardActionsAccess(
NeoForgeNativeRewardActions actions = new NeoForgeNativeRewardActions(
server, scheduler, new NeoForgePlayerDirectory());
NeoForgeDeferredVote vote = new NeoForgeDeferredVote(UUID.randomUUID(), UUID.randomUUID(),
"Alex", "Service", "Supported", 100L, true, true, true);
@@ -1202,7 +1185,7 @@ class NeoForgeRewardReplayServiceTest {
java.util.concurrent.ExecutionException failure = assertThrows(
java.util.concurrent.ExecutionException.class,
() -> completion.get(5, TimeUnit.SECONDS));
assertTrue(ReflectiveAccess.type("com.bencodez.votingplugin.neoforge.NeoForgeNativeRewardActions$UncertainRewardOutcomeException").isInstance(failure.getCause()));
assertTrue(failure.getCause() instanceof NeoForgeNativeRewardActions.UncertainRewardOutcomeException);
assertEquals(1, server.commands.dispatches.get());
} finally {
scheduler.close();
@@ -1214,7 +1197,7 @@ class NeoForgeRewardReplayServiceTest {
NeoForgeServerScheduler scheduler = new NeoForgeServerScheduler();
CallbackCommandServer server = new CallbackCommandServer(true, true);
try {
NeoForgeNativeRewardActionsAccess actions = new NeoForgeNativeRewardActionsAccess(
NeoForgeNativeRewardActions actions = new NeoForgeNativeRewardActions(
server, scheduler, new NeoForgePlayerDirectory());
NeoForgeDeferredVote vote = new NeoForgeDeferredVote(UUID.randomUUID(), UUID.randomUUID(),
"Alex", "Service", "Supported", 100L, true, true, true);
@@ -1237,10 +1220,10 @@ class NeoForgeRewardReplayServiceTest {
NeoForgeServerScheduler scheduler = new NeoForgeServerScheduler();
NeoForgePlayerDirectory players = new NeoForgePlayerDirectory();
UUID playerId = UUID.randomUUID();
NeoForgePlayerDirectoryAccess.joined(players, new SharedVoteIdentity(playerId, "OldName", true));
players.joined(new SharedVoteIdentity(playerId, "OldName", true));
CallbackCommandServer server = new CallbackCommandServer(true, true);
try {
NeoForgeNativeRewardActionsAccess actions = new NeoForgeNativeRewardActionsAccess(server, scheduler, players);
NeoForgeNativeRewardActions actions = new NeoForgeNativeRewardActions(server, scheduler, players);
NeoForgeDeferredVote oldIdentity = new NeoForgeDeferredVote(UUID.randomUUID(), playerId,
"OldName", "Service", "Supported", 100L, true, true, true);
NeoForgeRewardPlan plan = new NeoForgeRewardPlan(NeoForgeRewardPlan.Status.READY,
@@ -1248,14 +1231,14 @@ class NeoForgeRewardReplayServiceTest {
NeoForgeRewardPlan.ActionType.CONSOLE_COMMAND, "say %player%")), false, "test");
CompletableFuture<Void> stale = actions.execute(oldIdentity, plan);
NeoForgePlayerDirectoryAccess.joined(players, new SharedVoteIdentity(playerId, "NewName", true));
players.joined(new SharedVoteIdentity(playerId, "NewName", true));
scheduler.onServerTick();
assertThrows(java.util.concurrent.ExecutionException.class,
() -> stale.get(5, TimeUnit.SECONDS));
assertEquals(0, server.commands.dispatches.get());
CompletableFuture<Void> refreshed = actions.execute(
NeoForgeDeferredVoteAccess.withReplayContext(oldIdentity, "NewName", "Service"), plan);
oldIdentity.withReplayContext("NewName", "Service"), plan);
scheduler.onServerTick();
refreshed.get(5, TimeUnit.SECONDS);
assertEquals(1, server.commands.dispatches.get());
@@ -1269,10 +1252,10 @@ class NeoForgeRewardReplayServiceTest {
NeoForgeServerScheduler scheduler = new NeoForgeServerScheduler();
NeoForgePlayerDirectory players = new NeoForgePlayerDirectory();
UUID playerId = UUID.randomUUID();
NeoForgePlayerDirectoryAccess.joined(players, new SharedVoteIdentity(playerId, "OldName", true));
players.joined(new SharedVoteIdentity(playerId, "OldName", true));
CallbackCommandServer server = new CallbackCommandServer(true, true);
try {
NeoForgeNativeRewardActionsAccess actions = new NeoForgeNativeRewardActionsAccess(server, scheduler, players);
NeoForgeNativeRewardActions actions = new NeoForgeNativeRewardActions(server, scheduler, players);
NeoForgeDeferredVote oldIdentity = new NeoForgeDeferredVote(UUID.randomUUID(), playerId,
"OldName", "Service", "Supported", 100L, true, true, true);
NeoForgeRewardPlan plan = new NeoForgeRewardPlan(NeoForgeRewardPlan.Status.READY,
@@ -1281,7 +1264,7 @@ class NeoForgeRewardReplayServiceTest {
CompletableFuture<Void> stale = actions.execute(oldIdentity, plan);
NeoForgeRuntimeTest.FakePlayer renamed = new NeoForgeRuntimeTest.FakePlayer(playerId, "NewName");
NeoForgePlayerDirectoryAccess.joined(players, renamed);
players.joined(renamed);
players.left(renamed);
scheduler.onServerTick();
@@ -1298,9 +1281,9 @@ class NeoForgeRewardReplayServiceTest {
NeoForgeServerScheduler scheduler = new NeoForgeServerScheduler();
NeoForgePlayerDirectory players = new NeoForgePlayerDirectory();
FakeMessagePlayer player = new FakeMessagePlayer(UUID.randomUUID());
NeoForgePlayerDirectoryAccess.joined(players, player);
players.joined(player);
try {
NeoForgeNativeRewardActionsAccess actions = new NeoForgeNativeRewardActionsAccess(new Object(), scheduler, players);
NeoForgeNativeRewardActions actions = new NeoForgeNativeRewardActions(new Object(), scheduler, players);
NeoForgeDeferredVote vote = new NeoForgeDeferredVote(UUID.randomUUID(), player.uuid,
"Alex", "Service", "Supported", 100L, true, true, true);
NeoForgeRewardPlan plan = new NeoForgeRewardPlan(NeoForgeRewardPlan.Status.READY,
@@ -1313,7 +1296,7 @@ class NeoForgeRewardReplayServiceTest {
java.util.concurrent.ExecutionException failure = assertThrows(
java.util.concurrent.ExecutionException.class,
() -> completion.get(5, TimeUnit.SECONDS));
assertTrue(ReflectiveAccess.type("com.bencodez.votingplugin.neoforge.NeoForgeNativeRewardActions$UncertainRewardOutcomeException").isInstance(failure.getCause()));
assertTrue(failure.getCause() instanceof NeoForgeNativeRewardActions.UncertainRewardOutcomeException);
assertEquals(1, player.deliveries.get());
} finally {
scheduler.close();
@@ -1325,13 +1308,13 @@ class NeoForgeRewardReplayServiceTest {
writeConfiguration(false);
UUID playerId = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
retain(runtime, voteId, playerId, "Service");
NeoForgeRewardActionsAccess uncertainActions = (vote, plan) -> CompletableFuture.failedFuture(
(Throwable) ReflectiveAccess.construct("com.bencodez.votingplugin.neoforge.NeoForgeNativeRewardActions$UncertainRewardOutcomeException", new String[]{"String"}, new Object[]{"uncertain"}));
try (NeoForgeRewardReplayService replay = NeoForgeRewardReplayServiceAccess.create(runtime.voteConfiguration(),
new NeoForgeRewardConfigurationAccess(runtime.config(), runtime.voteSites(), runtime.specialRewards()),
NeoForgeRewardActions uncertainActions = (vote, plan) -> CompletableFuture.failedFuture(
new NeoForgeNativeRewardActions.UncertainRewardOutcomeException("uncertain"));
try (NeoForgeRewardReplayService replay = new NeoForgeRewardReplayService(runtime.voteConfiguration(),
new NeoForgeRewardConfiguration(runtime.config(), runtime.voteSites(), runtime.specialRewards()),
runtime.accounting(), runtime.deferredVotes(), runtime.players(), uncertainActions)) {
CompletableFuture<List<NeoForgeRewardReplayService.ReplayResult>> result = replay.replayOnce();
assertEquals(NeoForgeRewardReplayService.Status.REWARD_UNCERTAIN,
@@ -1343,9 +1326,9 @@ class NeoForgeRewardReplayServiceTest {
}
RecordingActions actions = new RecordingActions();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeRewardReplayService replay = service(runtime, actions)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(playerId, "Alex", true));
runtime.players().joined(new SharedVoteIdentity(playerId, "Alex", true));
List<NeoForgeRewardReplayService.ReplayResult> result = replay.replayOnce().get(5, TimeUnit.SECONDS);
assertEquals(NeoForgeRewardReplayService.Status.REWARD_UNCERTAIN, result.get(0).status());
assertEquals(0, actions.calls.get());
@@ -1356,12 +1339,12 @@ class NeoForgeRewardReplayServiceTest {
private NeoForgeRewardReplayService service(NeoForgeRuntime runtime, RecordingActions actions) {
actions.scheduler = runtime.scheduler();
return service(runtime, (NeoForgeRewardActionsAccess) actions);
return service(runtime, (NeoForgeRewardActions) actions);
}
private NeoForgeRewardReplayService service(NeoForgeRuntime runtime, NeoForgeRewardActionsAccess actions) {
return NeoForgeRewardReplayServiceAccess.create(runtime.voteConfiguration(),
new NeoForgeRewardConfigurationAccess(runtime.config(), runtime.voteSites(), runtime.specialRewards()),
private NeoForgeRewardReplayService service(NeoForgeRuntime runtime, NeoForgeRewardActions actions) {
return new NeoForgeRewardReplayService(runtime.voteConfiguration(),
new NeoForgeRewardConfiguration(runtime.config(), runtime.voteSites(), runtime.specialRewards()),
runtime.accounting(), runtime.deferredVotes(), runtime.players(), actions);
}
@@ -1443,7 +1426,7 @@ class NeoForgeRewardReplayServiceTest {
""".replace("__REWARD__", rewards));
}
private static final class RecordingActions implements NeoForgeRewardActionsAccess {
private static final class RecordingActions implements NeoForgeRewardActions {
final AtomicInteger calls = new AtomicInteger();
final List<String> rendered = new ArrayList<>();
final CountDownLatch scheduled = new CountDownLatch(1);
@@ -1455,7 +1438,7 @@ class NeoForgeRewardReplayServiceTest {
@Override public CompletableFuture<Void> execute(NeoForgeDeferredVote vote, NeoForgeRewardPlan plan) {
calls.incrementAndGet();
if (throwSynchronously) throw new IllegalStateException("expected synchronous failure");
CompletableFuture<Void> result = NeoForgeServerSchedulerAccess.executeAsync(scheduler, () -> {
CompletableFuture<Void> result = scheduler.executeAsync(() -> {
executionThread.set(Thread.currentThread());
if (fail) throw new IllegalStateException("expected");
plan.actions().forEach(action -> rendered.add(replace(action.value(), vote)));
@@ -1,9 +1,4 @@
package com.bencodez.votingplugin.tests.neoforge;
import com.bencodez.votingplugin.neoforge.NeoForgePlayerDirectory;
import com.bencodez.votingplugin.neoforge.NeoForgeRuntime;
import com.bencodez.votingplugin.neoforge.NeoForgeServerScheduler;
import com.bencodez.votingplugin.neoforge.NeoForgeVotingPlugin;
package com.bencodez.votingplugin.neoforge;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -27,7 +22,7 @@ class NeoForgeRuntimeTest {
@Test
void startsWithExistingConfigurationAndStorageThenClosesCleanly() throws IOException {
NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
try {
assertTrue(Files.isRegularFile(directory.resolve("Config.yml")));
assertTrue(Files.isRegularFile(directory.resolve("VoteSites.yml")));
@@ -41,7 +36,7 @@ class NeoForgeRuntimeTest {
assertFalse(runtime.storage().isOpen());
assertThrows(RejectedExecutionException.class, () -> runtime.scheduler().execute(() -> {}));
try (NeoForgeRuntime restarted = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime restarted = NeoForgeRuntime.start(directory)) {
assertTrue(restarted.storage().isOpen());
}
} finally {
@@ -52,7 +47,7 @@ class NeoForgeRuntimeTest {
@Test
void rejectsUnsupportedStorageWithoutStartingSqlite() throws IOException {
Files.writeString(directory.resolve("Config.yml"), "DataStorage: MYSQL\n");
IOException failure = assertThrows(IOException.class, () -> NeoForgeRuntimeAccess.start(directory));
IOException failure = assertThrows(IOException.class, () -> NeoForgeRuntime.start(directory));
assertTrue(failure.getMessage().contains("only SQLITE"));
assertFalse(Files.exists(directory.resolve("VotingPlugin.db")));
}
@@ -81,24 +76,24 @@ class NeoForgeRuntimeTest {
NeoForgePlayerDirectory players = new NeoForgePlayerDirectory();
UUID uuid = UUID.randomUUID();
FakePlayer player = new FakePlayer(uuid, "Ben");
NeoForgePlayerDirectoryAccess.joined(players, player);
players.joined(player);
assertEquals("Ben", players.online(uuid).orElseThrow().playerName());
assertTrue(players.online(uuid).orElseThrow().online());
assertEquals(player, NeoForgePlayerDirectoryAccess.playerFromEvent(new FakeEvent(player)));
assertEquals(player, NeoForgePlayerDirectory.playerFromEvent(new FakeEvent(player)));
players.left(player);
assertTrue(players.online(uuid).isEmpty());
assertEquals("Ben", NeoForgePlayerDirectoryAccess.latestName(players, uuid).orElseThrow());
assertEquals("Ben", players.latestName(uuid).orElseThrow());
players.clear();
assertTrue(NeoForgePlayerDirectoryAccess.latestName(players, uuid).isEmpty());
assertTrue(players.latestName(uuid).isEmpty());
}
@Test
void runtimePlayerJoinPersistsUuidBoundIdentityAcrossRestart() throws IOException {
UUID uuid = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory, new Object())) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory, new Object())) {
runtime.playerJoined(new FakePlayer(uuid, "CurrentName"));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
assertEquals("CurrentName", runtime.accounting().load(uuid).orElseThrow().playerName());
}
}
@@ -115,7 +110,7 @@ class NeoForgeRuntimeTest {
IllegalStateException first = new IllegalStateException("socket close failed");
IllegalArgumentException second = new IllegalArgumentException("later close failed");
RuntimeException thrown = NeoForgeRuntimeAccess.closeAll(
RuntimeException thrown = NeoForgeRuntime.closeAll(
() -> { throw first; },
closed::incrementAndGet,
() -> { throw second; },
@@ -134,7 +129,7 @@ class NeoForgeRuntimeTest {
IllegalArgumentException firstCleanup = new IllegalArgumentException("proxy close failed");
IllegalStateException secondCleanup = new IllegalStateException("replay close failed");
RuntimeException cleanup = NeoForgeRuntimeAccess.closeAll(
RuntimeException cleanup = NeoForgeRuntime.closeAll(
() -> { throw firstCleanup; },
closed::incrementAndGet,
() -> { throw secondCleanup; },
@@ -1,8 +1,4 @@
package com.bencodez.votingplugin.tests.neoforge;
import com.bencodez.votingplugin.neoforge.NeoForgeRuntime;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteAccount;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteSite;
package com.bencodez.votingplugin.neoforge;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -36,7 +32,7 @@ class NeoForgeVoteAccountingStoreTest {
UUID playerId = UUID.randomUUID();
long voteTime = 123_456L;
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteSite site = runtime.voteConfiguration().resolveEnabledSite("service.example").orElseThrow();
SharedVoteIdentity identity = new SharedVoteIdentity(playerId, "Alex", true);
SharedVoteInput input = vote("Alex", voteTime, true, true);
@@ -47,7 +43,7 @@ class NeoForgeVoteAccountingStoreTest {
assertAccount(account, "Alex", 1, 1, 1, 1, 3, voteTime);
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteAccount persisted = runtime.accounting().load(playerId).orElseThrow();
assertAccount(persisted, "Alex", 1, 1, 1, 1, 3, voteTime);
@@ -63,7 +59,7 @@ class NeoForgeVoteAccountingStoreTest {
@Test
void sharedPolicyControlsTotalsAndPointsWithoutChangingTimestampPersistence() throws IOException {
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteSite site = site();
List<Scenario> scenarios = List.of(
new Scenario("real", true, true, true, true, true, 1, 2),
@@ -98,7 +94,7 @@ class NeoForgeVoteAccountingStoreTest {
@Test
void missingUserIsNotMaterializedByRead() throws IOException {
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
assertFalse(runtime.accounting().load(UUID.randomUUID()).isPresent());
assertTrue(runtime.storage().enumerateUsers().isEmpty());
}
@@ -107,7 +103,7 @@ class NeoForgeVoteAccountingStoreTest {
@Test
void updatingAnotherSiteRetainsMalformedLastVoteAsBukkitZeroTimestamp() throws IOException {
writeVoteSites("MalformedSite", "Other");
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
UUID uuid = UUID.randomUUID();
runtime.storage().user(uuid).write(UserStorage.SQLITE, "LastVotes",
new DataValueString("MalformedSite//not-a-number"));
@@ -130,7 +126,7 @@ class NeoForgeVoteAccountingStoreTest {
void updatingAnotherSitePrunesRemovedLastVoteAcrossRestart() throws IOException {
writeVoteSites("CurrentSite", "UpdateSite");
UUID uuid = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.storage().user(uuid).write(UserStorage.SQLITE, "LastVotes",
new DataValueString("CurrentSite//100%line%RemovedSite//200"));
NeoForgeVoteSite updateSite = runtime.voteConfiguration()
@@ -147,7 +143,7 @@ class NeoForgeVoteAccountingStoreTest {
assertFalse(account.lastVotes().containsKey("RemovedSite"));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteAccount persisted = runtime.accounting().load(uuid).orElseThrow();
assertEquals(100L, persisted.lastVote("CurrentSite"));
assertEquals(300L, persisted.lastVote("UpdateSite"));
@@ -1,8 +1,4 @@
package com.bencodez.votingplugin.tests.neoforge;
import com.bencodez.votingplugin.neoforge.NeoForgeRuntime;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteConfiguration;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteSite;
package com.bencodez.votingplugin.neoforge;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -22,7 +18,7 @@ class NeoForgeVoteConfigurationTest {
@Test
void packagedDefaultsExposeCountingPolicyAndDisabledExampleSite() throws IOException {
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteConfiguration config = runtime.voteConfiguration();
assertTrue(config.addTotals());
assertTrue(config.addTotalsOffline());
@@ -65,7 +61,7 @@ class NeoForgeVoteConfigurationTest {
VoteDelay: 24h
""");
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteConfiguration config = runtime.voteConfiguration();
NeoForgeVoteSite site = config.resolveEnabledSite("example.com").orElseThrow();
assertEquals("PrimaryKey", site.key());
@@ -101,13 +97,13 @@ class NeoForgeVoteConfigurationTest {
void restartingReloadsConfigurationFromDisk() throws IOException {
writeConfig(true, true, true, true, 1, -1, false, 5);
Files.writeString(directory.resolve("VoteSites.yml"), siteYaml("old.example"));
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
assertTrue(runtime.voteConfiguration().resolveEnabledSite("old.example").isPresent());
}
writeConfig(false, false, false, false, 4, 20, true, 7);
Files.writeString(directory.resolve("VoteSites.yml"), siteYaml("new.example"));
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteConfiguration config = runtime.voteConfiguration();
assertTrue(config.resolveEnabledSite("old.example").isEmpty());
assertTrue(config.resolveEnabledSite("new.example").isPresent());
@@ -136,7 +132,7 @@ class NeoForgeVoteConfigurationTest {
Priority: 100
""");
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteConfiguration config = runtime.voteConfiguration();
NeoForgeVoteSite resolved = config.resolveEnabledSite("shared.example").orElseThrow();
assertEquals("HigherPriority", resolved.key());
@@ -173,7 +169,7 @@ class NeoForgeVoteConfigurationTest {
votedelay: invalid duration
""");
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteConfiguration config = runtime.voteConfiguration();
NeoForgeVoteSite site = config.resolveEnabledSite("MIXED.EXAMPLE").orElseThrow();
assertEquals("Mixed Name", site.displayName());
@@ -1,10 +1,4 @@
package com.bencodez.votingplugin.tests.neoforge;
import com.bencodez.votingplugin.neoforge.NeoForgeRuntime;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteAccount;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteProcessor;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteRequest;
import com.bencodez.votingplugin.neoforge.NeoForgeVoteResult;
package com.bencodez.votingplugin.neoforge;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -31,14 +25,14 @@ class NeoForgeVoteProcessorTest {
void accountingOnlyVotePersistsThroughRestart() throws IOException {
writeConfiguration(true, true, true, true);
UUID uuid = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(uuid, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(uuid, "Alex", true));
NeoForgeVoteResult result = runtime.voteProcessor().process(request(uuid, "Service", 100L, true, true, true));
assertEquals(NeoForgeVoteResult.Status.ACCOUNTED, result.status());
assertAccount(result.account(), 1, 1);
assertEquals(100L, result.account().lastVotes().get("EnabledSite"));
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteAccount stored = runtime.accounting().load(uuid).orElseThrow();
assertAccount(stored, 1, 1);
assertEquals(100L, stored.lastVotes().get("EnabledSite"));
@@ -50,9 +44,9 @@ class NeoForgeVoteProcessorTest {
writeConfiguration(true, true, true, true);
UUID unknown = UUID.randomUUID();
UUID disabled = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(unknown, "Unknown", true));
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(disabled, "Disabled", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(unknown, "Unknown", true));
runtime.players().joined(new SharedVoteIdentity(disabled, "Disabled", true));
assertEquals(NeoForgeVoteResult.Status.UNKNOWN_SITE,
runtime.voteProcessor().process(request(unknown, "Unknown", 100L, true, true, true,
"MissingService")).status());
@@ -68,8 +62,8 @@ class NeoForgeVoteProcessorTest {
void fakeVoteUsesSharedCountingPolicy() throws IOException {
writeConfiguration(true, true, false, true);
UUID uuid = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(uuid, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(uuid, "Alex", true));
NeoForgeVoteResult result = runtime.voteProcessor().process(request(uuid, "Service", 100L, false, true, true));
assertEquals(NeoForgeVoteResult.Status.ACCOUNTED, result.status());
assertAccount(result.account(), 0, 0);
@@ -81,8 +75,8 @@ class NeoForgeVoteProcessorTest {
void totalsDisabledStillAwardsConfiguredPoints() throws IOException {
writeConfiguration(true, false, true, true);
UUID uuid = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(uuid, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(uuid, "Alex", true));
NeoForgeVoteResult result = runtime.voteProcessor().process(
request(uuid, "Service", 100L, true, true, true));
assertAccount(result.account(), 0, 1);
@@ -92,7 +86,7 @@ class NeoForgeVoteProcessorTest {
@Test
void offlineTotalsFollowConfiguredPolicyWhilePointsRemainEnabled() throws IOException {
writeConfiguration(true, true, true, false);
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
NeoForgeVoteResult result = runtime.voteProcessor().process(
request(UUID.randomUUID(), "Service", 100L, true, true, false));
assertAccount(result.account(), 0, 1);
@@ -103,8 +97,8 @@ class NeoForgeVoteProcessorTest {
void playerDirectoryIsAuthoritativeForOnlineTotals() throws IOException {
writeConfiguration(true, true, true, false);
UUID uuid = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(uuid, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(uuid, "Alex", true));
NeoForgeVoteResult result = runtime.voteProcessor().process(
request(uuid, "Alex", 100L, true, true, false));
assertAccount(result.account(), 1, 1);
@@ -128,8 +122,8 @@ class NeoForgeVoteProcessorTest {
Clock clock = Clock.fixed(Instant.parse("2026-01-02T03:00:00Z"), ZoneOffset.UTC);
long previousVote = Instant.parse("2026-01-01T23:30:00Z").toEpochMilli();
UUID uuid = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory, clock)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(uuid, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory, clock)) {
runtime.players().joined(new SharedVoteIdentity(uuid, "Alex", true));
assertEquals(NeoForgeVoteResult.Status.ACCOUNTED,
runtime.voteProcessor().process(request(uuid, "Alex", previousVote, true, true, true)).status());
NeoForgeVoteResult delayed = runtime.voteProcessor().process(
@@ -145,8 +139,8 @@ class NeoForgeVoteProcessorTest {
writeConfiguration(true, true, true, true);
UUID uuid = UUID.randomUUID();
long voteTime = System.currentTimeMillis();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(uuid, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(uuid, "Alex", true));
NeoForgeVoteResult first = runtime.voteProcessor().process(request(uuid, "Service", voteTime, true, true, true));
assertEquals(NeoForgeVoteResult.Status.ACCOUNTED, first.status());
NeoForgeVoteResult second = runtime.voteProcessor().process(request(uuid, "Service", voteTime + 1L, true, true, true));
@@ -162,8 +156,8 @@ class NeoForgeVoteProcessorTest {
writeConfiguration(true, true, true, true);
UUID uuid = UUID.randomUUID();
UUID voteId = UUID.randomUUID();
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
NeoForgePlayerDirectoryAccess.joined(runtime.players(), new SharedVoteIdentity(uuid, "Alex", true));
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
runtime.players().joined(new SharedVoteIdentity(uuid, "Alex", true));
NeoForgeVoteRequest request = new NeoForgeVoteRequest(voteId, uuid, "Alex", "Service", 100L,
true, true, true, NeoForgeVoteRequest.Scope.COMPLETE);
NeoForgeVoteResult result = runtime.voteProcessor().process(request);
@@ -173,7 +167,7 @@ class NeoForgeVoteProcessorTest {
assertNoAccounting(runtime.accounting().load(uuid).orElseThrow());
assertEquals(voteId, runtime.deferredVotes().pending(uuid).get(0).voteId());
}
try (NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory)) {
try (NeoForgeRuntime runtime = NeoForgeRuntime.start(directory)) {
assertNoAccounting(runtime.accounting().load(uuid).orElseThrow());
assertEquals(voteId, runtime.deferredVotes().pending(uuid).get(0).voteId());
}
@@ -183,7 +177,7 @@ class NeoForgeVoteProcessorTest {
void unknownOnlineIdentityAndStoppedRuntimeAreRejected() throws IOException {
writeConfiguration(false, true, true, true);
UUID uuid = UUID.randomUUID();
NeoForgeRuntime runtime = NeoForgeRuntimeAccess.start(directory);
NeoForgeRuntime runtime = NeoForgeRuntime.start(directory);
NeoForgeVoteProcessor processor = runtime.voteProcessor();
assertEquals(NeoForgeVoteResult.Status.UNKNOWN_PLAYER,
processor.process(request(uuid, "Alex", 100L, true, true, true)).status());
@@ -0,0 +1,203 @@
package com.bencodez.votingplugin.npc;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import com.bencodez.advancedcore.api.command.CommandHandler;
import com.bencodez.simpleapi.scheduler.BukkitScheduler;
import com.bencodez.votingplugin.VotingPluginMain;
class VotingNPCCommandsTest {
private static class Fixture {
final VotingPluginMain plugin = mock(VotingPluginMain.class, RETURNS_DEEP_STUBS);
final BukkitScheduler scheduler = mock(BukkitScheduler.class);
final VotingNPCManager manager = mock(VotingNPCManager.class);
final ArrayList<CommandHandler> handlers = new ArrayList<>();
final LinkedBlockingQueue<Runnable> tasks = new LinkedBlockingQueue<>();
final LinkedBlockingQueue<Runnable> asynchronous = new LinkedBlockingQueue<>();
Fixture() {
when(plugin.getAdminVoteCommand()).thenReturn(handlers);
when(plugin.getVotingNPCManager()).thenReturn(manager);
when(plugin.getBukkitScheduler()).thenReturn(scheduler);
when(manager.available()).thenReturn(true);
doAnswer(inv -> { tasks.add(inv.getArgument(1)); return null; })
.when(scheduler).runTask(eq(plugin), any(Runnable.class), any(Entity.class));
doAnswer(inv -> { tasks.add(inv.getArgument(1)); return null; })
.when(scheduler).runTask(eq(plugin), any(Runnable.class));
doAnswer(inv -> { asynchronous.add(inv.getArgument(1)); return null; })
.when(scheduler).runTaskAsynchronously(eq(plugin), any(Runnable.class));
VotingNPCCommands.register(plugin);
}
Player player() {
Player player = mock(Player.class);
when(player.isOnline()).thenReturn(true);
when(player.hasPermission("VotingPlugin.Commands.AdminVote.NPC")).thenReturn(true);
World world = mock(World.class); when(world.getUID()).thenReturn(new UUID(1, 0));
// Location holds its world weakly; retain it as a real online player does.
when(player.getWorld()).thenReturn(world);
when(player.getLocation()).thenReturn(new Location(world, 4, 64, 4));
return player;
}
CommandHandler command(String action, int arguments) {
return handlers.stream().filter(value -> value.getArgs()[1].equals(action)
&& value.getArgs().length == arguments).findFirst().orElseThrow();
}
}
@Test void usesOnlyDedicatedNpcPatternsAndExplicitAdminPermission() {
var f = new Fixture();
assertEquals(List.of(List.of("NPC", "Create", "(string)"), List.of("NPC", "Create", "(string)", "(list)"),
List.of("NPC", "List"), List.of("NPC", "Inspect", "(npcid)"), List.of("NPC", "Remove", "(npcid)")),
f.handlers.stream().map(value -> List.of(value.getArgs())).toList());
for (var handler : f.handlers) assertEquals("VotingPlugin.Commands.AdminVote.NPC|VotingPlugin.Admin", handler.getPerm());
}
@Test void metadataGrantsTheAdminRootGateWithoutGrantingOtherAdminLeaves() throws Exception {
org.bukkit.plugin.PluginDescriptionFile description;
try (var input = getClass().getResourceAsStream("/plugin.yml")) {
assertNotNull(input); description = new org.bukkit.plugin.PluginDescriptionFile(input);
}
var permissions = description.getPermissions().stream().collect(java.util.stream.Collectors.toMap(
permission -> permission.getName().toLowerCase(java.util.Locale.ROOT), java.util.function.Function.identity()));
var plugin = mock(VotingPluginMain.class); when(plugin.isEnabled()).thenReturn(true);
var pm = mock(org.bukkit.plugin.PluginManager.class);
when(pm.getDefaultPermissions(false)).thenReturn(java.util.Set.of());
when(pm.getPermission(anyString())).thenAnswer(call -> permissions.get(((String)call.getArgument(0)).toLowerCase(java.util.Locale.ROOT)));
try (var bukkit = mockStatic(org.bukkit.Bukkit.class)) {
bukkit.when(org.bukkit.Bukkit::getPluginManager).thenReturn(pm);
var server = mock(org.bukkit.Server.class); when(server.getPluginManager()).thenReturn(pm);
bukkit.when(org.bukkit.Bukkit::getServer).thenReturn(server);
for (String grant : List.of("VotingPlugin.Commands.AdminVote.NPC", "VotingPlugin.Admin")) {
var permissible = new org.bukkit.permissions.PermissibleBase(mock(org.bukkit.permissions.ServerOperator.class));
permissible.addAttachment(plugin, grant, true);
CommandSender sender = mock(CommandSender.class);
when(sender.hasPermission(anyString())).thenAnswer(call -> permissible.hasPermission((String)call.getArgument(0)));
var root = new org.bukkit.command.Command("av") {
@Override public boolean execute(CommandSender sender, String label, String[] args) { return true; }
};
root.setPermission("VotingPlugin.Commands.AdminVote");
assertTrue(root.testPermissionSilent(sender), grant);
if (grant.endsWith(".NPC")) {
assertFalse(permissible.hasPermission("VotingPlugin.Commands.AdminVote.Edit.Config"));
assertFalse(permissible.hasPermission("VotingPlugin.Admin"));
}
}
}
}
@Test void capturesPlayerStateOnlyOnOwnerAndSuccessOnlyAfterDurableCompletion() {
var f = new Fixture(); Player player = f.player();
var completion = new CompletableFuture<VotingNPCDefinition>();
when(f.manager.create(any())).thenReturn(completion);
f.command("Create", 4).execute(player, new String[] { "NPC", "Create", "voting", "Vote", "here" });
verify(player, never()).getLocation(); verify(f.manager, never()).create(any());
f.tasks.remove().run();
var definition = ArgumentCaptor.forClass(VotingNPCDefinition.class); verify(f.manager).create(definition.capture());
assertEquals("Vote here", definition.getValue().name()); assertEquals("voting", definition.getValue().id());
verify(player, never()).sendMessage(anyString());
completion.complete(definition.getValue());
verify(player, never()).sendMessage(anyString());
f.tasks.remove().run();
verify(player).sendMessage(contains("Saved voting NPC 'voting'"));
}
@Test void delayedPermissionRevocationAndDisabledFeaturePreventMutation() {
var f = new Fixture(); Player player = f.player();
var handler = f.command("Create", 3);
handler.execute(player, new String[] { "NPC", "Create", "voting" });
when(player.hasPermission("VotingPlugin.Commands.AdminVote.NPC")).thenReturn(false);
f.tasks.remove().run(); verify(f.manager, never()).create(any());
when(player.hasPermission("VotingPlugin.Commands.AdminVote.NPC")).thenReturn(true);
when(f.manager.available()).thenReturn(false);
handler.execute(player, new String[] { "NPC", "Create", "voting" }); f.tasks.remove().run();
verify(f.manager, never()).create(any()); verify(player).sendMessage(contains("VotingNPCs.Enabled"));
}
@Test void rejectedStorageDoesNotSendSuccessAndDoesNotLeakRawFailure() {
var f = new Fixture(); Player player = f.player();
when(f.manager.remove("voting")).thenReturn(CompletableFuture.failedFuture(new IllegalStateException("Synthetic private diagnostic")));
f.command("Remove", 3).execute(player, new String[] { "NPC", "Remove", "voting" });
f.tasks.remove().run(); f.tasks.remove().run();
verify(player).sendMessage(contains("was not confirmed"));
verify(player, never()).sendMessage(contains("Synthetic private"));
verify(player, never()).sendMessage(startsWith("Removed saved"));
}
@Test void validationRejectionsHaveFixedActionableMessagesAndUnavailableStorageStaysConservative() {
for (var reason : VotingNPCRegistry.Rejection.values()) {
var f = new Fixture(); Player player = f.player();
when(f.manager.create(any())).thenReturn(CompletableFuture.failedFuture(
new java.util.concurrent.CompletionException(new VotingNPCRegistry.ValidationFailure(reason))));
f.command("Create", 3).execute(player, new String[] { "NPC", "Create", "voting" });
f.tasks.remove().run(); f.tasks.remove().run();
String expected = switch (reason) {
case DUPLICATE_ID -> "ID already exists";
case DUPLICATE_IDENTITY -> "identity already exists";
case LIMIT -> "64 voting NPC limit";
};
verify(player).sendMessage(contains(expected));
verify(player, never()).sendMessage(contains("reload before retrying"));
}
var f = new Fixture(); Player player = f.player();
var pending = new CompletableFuture<VotingNPCDefinition>();
when(f.manager.create(any())).thenReturn(pending);
f.command("Create", 3).execute(player, new String[] { "NPC", "Create", "voting" }); f.tasks.remove().run();
when(f.manager.available()).thenReturn(false);
pending.completeExceptionally(new java.util.concurrent.RejectedExecutionException("Synthetic")); f.tasks.remove().run();
verify(player).sendMessage(contains("reload before retrying"));
verify(player, never()).sendMessage(contains("busy"));
}
@Test void healthyQueueSaturationDoesNotTellTheAdministratorToReloadStorage() {
var f = new Fixture(); Player player = f.player();
when(f.manager.create(any())).thenReturn(CompletableFuture.failedFuture(
new java.util.concurrent.RejectedExecutionException("Synthetic queue rejection")));
f.command("Create", 3).execute(player, new String[] { "NPC", "Create", "voting" });
f.tasks.remove().run(); f.tasks.remove().run();
verify(player).sendMessage(contains("busy"));
verify(player, never()).sendMessage(contains("reload before retrying"));
verify(player, never()).sendMessage(contains("Synthetic"));
}
@Test void invalidDefinitionAndConsoleCreationDoNotMutate() {
var f = new Fixture(); Player player = f.player();
f.command("Create", 3).execute(player, new String[] { "NPC", "Create", "bad id" }); f.tasks.remove().run();
verify(f.manager, never()).create(any()); verify(player).sendMessage(contains("lowercase ID"));
CommandSender console = mock(CommandSender.class); when(console.hasPermission("VotingPlugin.Admin")).thenReturn(true);
f.command("Create", 3).execute(console, new String[] { "NPC", "Create", "voting" }); f.tasks.remove().run();
verify(console).sendMessage("Create a voting NPC from in game."); verify(f.manager, never()).create(any());
}
@Test void bothCreationFormsMatchRealCommandDispatchWithStandardFrameworkPlaceholders() {
var tabs = new com.bencodez.simpleapi.command.TabCompleteHandler();
// These are the standard placeholders AdvancedCore registers before commands.
tabs.addTabCompleteOption("(String)", new ArrayList<>());
tabs.addTabCompleteOption("(List)", new ArrayList<>());
try (var tabSingleton = mockStatic(com.bencodez.simpleapi.command.TabCompleteHandler.class)) {
tabSingleton.when(com.bencodez.simpleapi.command.TabCompleteHandler::getInstance).thenReturn(tabs);
var f = new Fixture(); Player player = f.player();
when(f.manager.create(any())).thenReturn(new CompletableFuture<>());
assertTrue(f.command("Create", 3).runCommand(player, new String[] { "npc", "create", "new-voting" }));
f.asynchronous.remove().run(); f.tasks.remove().run();
assertTrue(f.command("Create", 4).runCommand(player, new String[] { "npc", "create", "another", "Vote", "here" }));
f.asynchronous.remove().run(); f.tasks.remove().run();
var values = ArgumentCaptor.forClass(VotingNPCDefinition.class); verify(f.manager, times(2)).create(values.capture());
assertEquals(List.of("new-voting", "another"), values.getAllValues().stream().map(VotingNPCDefinition::id).toList());
}
}
}
@@ -0,0 +1,556 @@
package com.bencodez.votingplugin.npc;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.mockito.AdditionalMatchers.aryEq;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Logger;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import com.bencodez.simpleapi.scheduler.BukkitScheduler;
import com.bencodez.votingplugin.VotingPluginMain;
class VotingNPCManagerTest {
@TempDir Path directory;
private static final UUID WORLD = new UUID(1, 0);
private static VotingNPCDefinition definition(int identity) {
return new VotingNPCDefinition("voting", new UUID(0, identity), WORLD, 8, 64, 8, 0, 0, "&aVote");
}
private static VotingNPCDefinition definitionAt(int identity, double x) {
return new VotingNPCDefinition("npc-" + identity, new UUID(0, identity), WORLD, x, 64, 8, 0, 0, "&aVote");
}
private static <T> T await(CompletionStage<T> stage) throws Exception {
return stage.toCompletableFuture().get(5, TimeUnit.SECONDS);
}
private static class Scheduler extends VotingNPCScheduler {
final LinkedBlockingQueue<Runnable> global = new LinkedBlockingQueue<>();
final LinkedBlockingQueue<Runnable> regions = new LinkedBlockingQueue<>();
final LinkedBlockingQueue<Runnable> entities = new LinkedBlockingQueue<>();
String owner = "none";
boolean rejectCleanup;
Scheduler(VotingPluginMain plugin) { super(plugin); }
@Override boolean owns(Location value) { return owner.equals("region"); }
@Override boolean owns(Entity value) { return owner.equals("entity"); }
@Override void global(Runnable action, Consumer<Throwable> failed) { global.add(action); }
@Override void region(Location anchor, Runnable action, Consumer<Throwable> failed) { regions.add(action); }
@Override void entity(Entity entity, Runnable action, Runnable retired, Consumer<Throwable> failed) {
if (rejectCleanup) failed.accept(new IllegalStateException("Synthetic rejection"));
else entities.add(action);
}
void runNext(LinkedBlockingQueue<Runnable> queue, String phase) throws Exception {
Runnable action = queue.poll(5, TimeUnit.SECONDS);
assertNotNull(action, "Expected scheduled " + phase + " work");
owner = phase;
try { action.run(); } finally { owner = "none"; }
}
}
@Test void duplicateChunkNotificationsDoNotReconstructAnotherEntityAndOrdinaryEntitiesAreIgnored() throws Exception {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
var scheduler = new Scheduler(plugin);
List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("npcs.json")); await(registry.initialLoad());
await(registry.create(definition(1)));
var manager = new VotingNPCManager(plugin, scheduler, registry, player -> fail("Unexpected menu"));
try {
manager.configure(true);
scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.regions, "region");
Chunk chunk = chunk(world);
manager.chunkLoad(new ChunkLoadEvent(chunk, false));
manager.chunkLoad(new ChunkLoadEvent(chunk, false));
assertEquals(1, spawned.size()); assertEquals(1, manager.runtimeCount());
verify(spawned.getFirst()).setPersistent(false);
verify(spawned.getFirst()).setAI(false);
verify(spawned.getFirst()).setCustomName("\u00a7aVote");
Player player = mock(Player.class); when(player.getUniqueId()).thenReturn(new UUID(0, 100));
Villager unrelated = mock(Villager.class); when(unrelated.getUniqueId()).thenReturn(new UUID(0, 99));
var interact = new PlayerInteractEntityEvent(player, unrelated, EquipmentSlot.HAND);
manager.interact(interact); assertFalse(interact.isCancelled());
var damage = mock(EntityDamageEvent.class); when(damage.getEntity()).thenReturn(unrelated);
manager.damage(damage); verify(damage, never()).setCancelled(anyBoolean());
var protectedDamage = mock(EntityDamageEvent.class); when(protectedDamage.getEntity()).thenReturn(spawned.getFirst());
manager.damage(protectedDamage); verify(protectedDamage).setCancelled(true);
var hostileTarget = new org.bukkit.event.entity.EntityTargetEvent(unrelated, spawned.getFirst(),
org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_ENTITY);
manager.target(hostileTarget);
assertTrue(hostileTarget.isCancelled(), "ordinary mobs must not target a tracked NPC");
var normalTarget = new org.bukkit.event.entity.EntityTargetEvent(unrelated, player,
org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_ENTITY);
manager.target(normalTarget); assertFalse(normalTarget.isCancelled());
var clearedTarget = new org.bukkit.event.entity.EntityTargetEvent(unrelated, null,
org.bukkit.event.entity.EntityTargetEvent.TargetReason.FORGOT_TARGET);
manager.target(clearedTarget); assertFalse(clearedTarget.isCancelled());
var witch = mock(org.bukkit.entity.Witch.class);
var transform = new org.bukkit.event.entity.EntityTransformEvent(spawned.getFirst(), List.of(witch),
org.bukkit.event.entity.EntityTransformEvent.TransformReason.LIGHTNING);
var unrelatedTransform = new org.bukkit.event.entity.EntityTransformEvent(unrelated, List.of(witch),
org.bukkit.event.entity.EntityTransformEvent.TransformReason.LIGHTNING);
// Discover actual listener methods, as event registration does;
// absence of a transformation handler must fail this regression.
for (var method : VotingNPCManager.class.getDeclaredMethods()) {
if (method.isAnnotationPresent(org.bukkit.event.EventHandler.class) && method.getParameterCount() == 1
&& method.getParameterTypes()[0].isInstance(transform)) {
method.invoke(manager, transform); method.invoke(manager, unrelatedTransform);
}
}
assertTrue(transform.isCancelled(), "lightning must not replace a tracked villager with an untracked entity");
assertFalse(unrelatedTransform.isCancelled());
} finally {
manager.close(); scheduler.runNext(scheduler.entities, "entity");
assertEquals(0, manager.runtimeCount()); assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS));
}
}
}
@Test void freedFullCapacityWakesOnePendingDifferentId() throws Exception {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
var scheduler = new Scheduler(plugin); List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("capacity.json")); await(registry.initialLoad());
for (int i = 1; i <= VotingNPCStore.MAX_NPCS; i++) await(registry.create(definitionAt(i, i * 2)));
var manager = new VotingNPCManager(plugin, scheduler, registry, ignored -> { });
try {
manager.configure(true); scheduler.runNext(scheduler.global, "global");
for (int i = 0; i < VotingNPCStore.MAX_NPCS; i++) scheduler.runNext(scheduler.regions, "region");
assertEquals(VotingNPCStore.MAX_NPCS, manager.runtimeCount());
await(manager.remove("npc-1")); await(manager.create(definitionAt(1000, 2000)));
scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.global, "global");
scheduler.runNext(scheduler.entities, "entity");
scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.regions, "region");
assertEquals(VotingNPCStore.MAX_NPCS, manager.runtimeCount());
assertEquals(VotingNPCStore.MAX_NPCS + 1, spawned.size());
} finally {
manager.close(); while (!scheduler.entities.isEmpty()) scheduler.runNext(scheduler.entities, "entity");
assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS));
}
}
}
@Test void unloadedCurrentReservationWakesDifferentPendingIdWithoutRetryingItself() throws Exception {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
var scheduler = new Scheduler(plugin); List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("unloaded-current-capacity.json")); await(registry.initialLoad());
for (int i = 1; i < VotingNPCStore.MAX_NPCS; i++) await(registry.create(definitionAt(i, i * 2)));
var manager = new VotingNPCManager(plugin, scheduler, registry, ignored -> { });
try {
manager.configure(true); scheduler.runNext(scheduler.global, "global");
for (int i = 1; i < VotingNPCStore.MAX_NPCS; i++) scheduler.runNext(scheduler.regions, "region");
await(manager.create(definitionAt(64, 128)));
scheduler.runNext(scheduler.global, "global");
assertEquals(64, manager.runtimeCount()); assertEquals(1, scheduler.regions.size());
await(manager.remove("npc-1")); await(manager.create(definitionAt(1000, 2000)));
scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.global, "global");
assertEquals(1, scheduler.entities.size()); assertTrue(scheduler.global.isEmpty());
assertEquals(1, scheduler.regions.size());
// The existing queued reservation is now unloaded; the new target remains loaded.
when(world.isChunkLoaded(anyInt(), anyInt())).thenAnswer(call -> (Integer) call.getArgument(0) != 8);
scheduler.runNext(scheduler.regions, "region");
assertEquals(63, manager.runtimeCount());
assertEquals(1, scheduler.global.size(), "Released capacity must wake the different waiting ID");
scheduler.runNext(scheduler.global, "global");
assertEquals(1, scheduler.regions.size(), "Only the pending ID may retry, not the unloaded reservation");
scheduler.runNext(scheduler.regions, "region");
assertEquals(64, spawned.size()); assertEquals(64, manager.runtimeCount());
assertTrue(scheduler.global.isEmpty()); assertTrue(scheduler.regions.isEmpty());
verify(world).spawn(argThat(anchor -> anchor.getX() == 2000), eq(Villager.class), any(Consumer.class));
} finally {
manager.close(); while (!scheduler.entities.isEmpty()) scheduler.runNext(scheduler.entities, "entity");
assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS));
}
}
}
@Test void successorWaitsForExactPredecessorCleanupAndCleanupRejectionRetainsOwnership() throws Exception {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
var scheduler = new Scheduler(plugin); List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("npcs.json")); await(registry.initialLoad());
await(registry.create(definition(1)));
var manager = new VotingNPCManager(plugin, scheduler, registry, ignored -> { });
try {
manager.configure(true); scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.regions, "region");
scheduler.rejectCleanup = true;
assertTrue(await(manager.remove("voting")));
await(manager.create(definition(2)));
// Process both durable mutation notifications while old removal is rejected.
scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.global, "global");
assertEquals(1, spawned.size()); assertEquals(1, manager.runtimeCount());
verify(spawned.getFirst(), never()).remove();
// A rejected cleanup keeps ownership; repeated load notifications may
// request a retry, but must never create a successor or duplicate task.
manager.chunkLoad(new ChunkLoadEvent(chunk(world), false));
manager.chunkLoad(new ChunkLoadEvent(chunk(world), false));
assertEquals(1, spawned.size()); assertEquals(1, manager.runtimeCount());
scheduler.rejectCleanup = false;
manager.chunkLoad(new ChunkLoadEvent(chunk(world), false));
scheduler.runNext(scheduler.entities, "entity");
verify(spawned.getFirst()).remove();
scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.regions, "region");
assertEquals(2, spawned.size()); assertEquals(1, manager.runtimeCount());
} finally {
scheduler.rejectCleanup = false; manager.close();
if (!scheduler.entities.isEmpty()) scheduler.runNext(scheduler.entities, "entity");
assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS));
}
}
}
@Test void transientUnloadCleanupRetriesAtLoadAndCoalescesUntilRemovalFinishes() throws Exception {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
var scheduler = new Scheduler(plugin); List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("npcs.json")); await(registry.initialLoad());
await(registry.create(definition(1)));
var manager = new VotingNPCManager(plugin, scheduler, registry, ignored -> { });
try {
manager.configure(true); scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.regions, "region");
Chunk chunk = chunk(world); scheduler.rejectCleanup = true;
manager.chunkUnload(new ChunkUnloadEvent(chunk));
assertEquals(1, manager.runtimeCount()); assertTrue(scheduler.global.isEmpty());
assertTrue(scheduler.entities.isEmpty(), "rejection must not create an autonomous retry loop");
scheduler.rejectCleanup = false;
manager.chunkLoad(new ChunkLoadEvent(chunk, false));
manager.chunkLoad(new ChunkLoadEvent(chunk, false));
assertEquals(1, scheduler.entities.size(), "a queued exact predecessor cleanup must coalesce");
assertEquals(1, spawned.size());
scheduler.runNext(scheduler.entities, "entity"); verify(spawned.getFirst()).remove();
scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.regions, "region");
assertEquals(2, spawned.size()); assertEquals(1, manager.runtimeCount());
assertTrue(scheduler.entities.isEmpty()); assertTrue(scheduler.global.isEmpty());
} finally {
scheduler.rejectCleanup = false; manager.close();
if (!scheduler.entities.isEmpty()) scheduler.runNext(scheduler.entities, "entity");
assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS));
}
}
}
@Test void queuedRemovedReservationWakesPendingCapacityWithoutEntity() throws Exception {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
var scheduler = new Scheduler(plugin); List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("queued-capacity.json")); await(registry.initialLoad());
for (int i = 1; i <= VotingNPCStore.MAX_NPCS; i++) await(registry.create(definitionAt(i, i * 2)));
var manager = new VotingNPCManager(plugin, scheduler, registry, ignored -> { });
try {
manager.configure(true); scheduler.runNext(scheduler.global, "global");
await(manager.remove("npc-1")); await(manager.create(definitionAt(1000, 2000)));
scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.global, "global");
for (int i = 0; i <= VotingNPCStore.MAX_NPCS; i++) scheduler.runNext(scheduler.regions, "region");
assertTrue(scheduler.regions.isEmpty()); assertTrue(scheduler.global.isEmpty());
assertEquals(VotingNPCStore.MAX_NPCS, manager.runtimeCount());
assertEquals(VotingNPCStore.MAX_NPCS, spawned.size());
} finally {
manager.close(); while (!scheduler.entities.isEmpty()) scheduler.runNext(scheduler.entities, "entity");
assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS));
}
}
}
@Test void unloadedQueuedReservationWithPendingCapacityDoesNotRetry() throws Exception {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(false);
var scheduler = new Scheduler(plugin); List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("unloaded-capacity.json")); await(registry.initialLoad());
for (int i = 1; i <= VotingNPCStore.MAX_NPCS; i++) await(registry.create(definitionAt(i, i * 2)));
var manager = new VotingNPCManager(plugin, scheduler, registry, ignored -> { });
try {
manager.configure(true); scheduler.runNext(scheduler.global, "global");
await(manager.remove("npc-1")); await(manager.create(definitionAt(1000, 2000)));
scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.global, "global");
for (int i = 0; i <= VotingNPCStore.MAX_NPCS; i++) scheduler.runNext(scheduler.regions, "region");
assertEquals(0, spawned.size()); assertTrue(scheduler.global.isEmpty());
assertTrue(scheduler.regions.isEmpty()); assertEquals(0, manager.runtimeCount());
} finally { manager.close(); assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS)); }
}
}
@Test void unloadedChunksNeverSpawnOrLoadAndLaterLoadReconstructsOnce() throws Exception {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
var scheduler = new Scheduler(plugin); List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("npcs.json")); await(registry.initialLoad());
await(registry.create(definition(1)));
var manager = new VotingNPCManager(plugin, scheduler, registry, ignored -> { });
try {
manager.configure(true); scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.regions, "region");
assertTrue(spawned.isEmpty()); assertEquals(0, manager.runtimeCount());
verify(world, never()).getChunkAt(anyInt(), anyInt()); verify(world, never()).loadChunk(anyInt(), anyInt());
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
manager.chunkLoad(new ChunkLoadEvent(chunk(world), false)); scheduler.runNext(scheduler.regions, "region");
assertEquals(1, spawned.size());
manager.chunkUnload(new ChunkUnloadEvent(chunk(world))); scheduler.runNext(scheduler.entities, "entity");
assertTrue(scheduler.global.isEmpty(), "unload must not immediately retry reconstruction");
assertTrue(scheduler.regions.isEmpty());
manager.chunkLoad(new ChunkLoadEvent(chunk(world), false)); scheduler.runNext(scheduler.regions, "region");
assertEquals(2, spawned.size());
} finally { manager.close(); scheduler.runNext(scheduler.entities, "entity"); assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS)); }
}
}
@Test void partialSpawnFailureRemovesOnlyTheCapturedEntityAndCloseInvalidatesQueuedSpawns() throws Exception {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
var scheduler = new Scheduler(plugin); List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("npcs.json")); await(registry.initialLoad());
await(registry.create(definition(1)));
var manager = new VotingNPCManager(plugin, scheduler, registry, ignored -> { });
try {
manager.configure(true); scheduler.runNext(scheduler.global, "global");
// Close before a deferred creation runs: no entity may be created.
manager.configure(false); scheduler.runNext(scheduler.regions, "region");
assertTrue(spawned.isEmpty()); assertEquals(0, manager.runtimeCount());
manager.configure(true); scheduler.runNext(scheduler.global, "global");
doAnswer(invocation -> {
Villager entity = mock(Villager.class); when(entity.getUniqueId()).thenReturn(new UUID(0, 17));
doThrow(new IllegalStateException("Synthetic entity property failure")).when(entity).setAI(false);
spawned.add(entity);
Consumer<Villager> initializer = invocation.getArgument(2); initializer.accept(entity); return entity;
}).when(world).spawn(any(Location.class), eq(Villager.class), any(Consumer.class));
scheduler.runNext(scheduler.regions, "region");
assertEquals(1, manager.runtimeCount());
scheduler.runNext(scheduler.entities, "entity");
verify(spawned.getFirst()).remove(); assertEquals(0, manager.runtimeCount());
assertTrue(scheduler.global.isEmpty(), "failed spawn must not cause an automatic retry loop");
} finally { manager.close(); assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS)); }
}
}
@Test void occupiedNeighbourUnloadRetiresAndReloadReconstructsOnceAtBothCoordinateSigns() throws Exception {
for (double position : new double[] {15.75, -.25}) {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
var scheduler = new Scheduler(plugin); List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("boundary-" + (position > 0) + ".json"));
await(registry.initialLoad());
var definition = new VotingNPCDefinition("voting", new UUID(0, 1), WORLD, position, 64, position, 0, 0, "Vote");
await(registry.create(definition));
var manager = new VotingNPCManager(plugin, scheduler, registry, ignored -> { });
try {
manager.configure(true); scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.regions, "region");
manager.chunkUnload(new ChunkUnloadEvent(atChunk(world, 7, 7)));
assertTrue(scheduler.entities.isEmpty(), "unrelated chunk unload must leave NPC alone");
int neighbour = definition.chunkX() + 1;
manager.chunkUnload(new ChunkUnloadEvent(atChunk(world, neighbour, neighbour)));
assertEquals(1, scheduler.entities.size(), "occupied adjacent chunk must trigger entity-owned cleanup");
// Bukkit may still report the unloading chunk as loaded during its event.
manager.chunkLoad(new ChunkLoadEvent(atChunk(world, definition.chunkX(), definition.chunkZ()), false));
scheduler.runNext(scheduler.entities, "entity");
assertEquals(0, manager.runtimeCount()); assertTrue(scheduler.regions.isEmpty());
manager.chunkLoad(new ChunkLoadEvent(atChunk(world, neighbour, neighbour), false));
manager.chunkLoad(new ChunkLoadEvent(atChunk(world, neighbour, neighbour), false));
scheduler.runNext(scheduler.regions, "region");
assertEquals(2, spawned.size()); assertEquals(1, manager.runtimeCount());
verify(world, never()).getChunkAt(anyInt(), anyInt()); verify(world, never()).loadChunk(anyInt(), anyInt());
} finally {
manager.close(); if (!scheduler.entities.isEmpty()) scheduler.runNext(scheduler.entities, "entity");
assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS));
}
}
}
}
@Test void neighbourUnloadFenceSurvivesReconciliationAndBlocksDeferredCreation() throws Exception {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
var scheduler = new Scheduler(plugin); List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("pending-boundary.json")); await(registry.initialLoad());
await(registry.create(new VotingNPCDefinition("voting", new UUID(0, 1), WORLD, 15.75, 64, 15.75, 0, 0, "Vote")));
var manager = new VotingNPCManager(plugin, scheduler, registry, ignored -> { });
try {
manager.configure(true); scheduler.runNext(scheduler.global, "global");
manager.chunkUnload(new ChunkUnloadEvent(atChunk(world, 1, 1)));
scheduler.runNext(scheduler.regions, "region");
assertTrue(spawned.isEmpty(), "a deferred creation cannot cross an occupied chunk unload");
assertEquals(0, manager.runtimeCount());
await(manager.create(new VotingNPCDefinition("other", new UUID(0, 2), WORLD, 40, 64, 40, 0, 0, "Vote")));
scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.regions, "region");
assertEquals(1, spawned.size(), "unrelated mutation must not erase the first NPC's occupied-chunk fence");
assertTrue(scheduler.regions.isEmpty());
} finally {
manager.close(); while (!scheduler.entities.isEmpty()) scheduler.runNext(scheduler.entities, "entity");
assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS));
}
}
}
private static Chunk atChunk(World world, int x, int z) {
Chunk chunk = mock(Chunk.class); when(chunk.getWorld()).thenReturn(world);
when(chunk.getX()).thenReturn(x); when(chunk.getZ()).thenReturn(z); return chunk;
}
@Test void disabledFeatureDoesNotReadStorageOrRegisterAnyRuntimeWork() {
var plugin = mock(VotingPluginMain.class);
var scheduler = new Scheduler(plugin);
var manager = new VotingNPCManager(plugin, scheduler, null, ignored -> fail("Disabled menu"));
manager.configure(false);
assertFalse(manager.available()); assertEquals(0, manager.runtimeCount());
assertTrue(scheduler.global.isEmpty()); assertTrue(scheduler.regions.isEmpty());
verify(plugin, never()).getDataFolder();
manager.close(); manager.close();
}
@Test void worldUnloadPreservesDefinitionsAndAWorldLoadAfterCleanupRecreatesOnce() throws Exception {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
var scheduler = new Scheduler(plugin); List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("npcs.json")); await(registry.initialLoad());
await(registry.create(definition(1)));
var manager = new VotingNPCManager(plugin, scheduler, registry, ignored -> { });
try {
manager.configure(true); scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.regions, "region");
manager.worldUnload(new WorldUnloadEvent(world));
// A load before delayed cleanup must record demand, not spawn a duplicate.
manager.worldLoad(new WorldLoadEvent(world)); scheduler.runNext(scheduler.global, "global");
assertEquals(1, spawned.size());
scheduler.runNext(scheduler.entities, "entity"); scheduler.runNext(scheduler.global, "global");
scheduler.runNext(scheduler.regions, "region");
assertEquals(2, spawned.size()); assertEquals(List.of(definition(1)), manager.definitions());
} finally { manager.close(); scheduler.runNext(scheduler.entities, "entity"); assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS)); }
}
}
@Test void cancelledAndOffhandClicksDoNotOpenAndRemovalInvalidatesAQueuedPlayerCallback() throws Exception {
var plugin = mock(VotingPluginMain.class); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger());
var bukkitScheduler = mock(BukkitScheduler.class); when(plugin.getBukkitScheduler()).thenReturn(bukkitScheduler);
var playerCallbacks = new LinkedBlockingQueue<Runnable>();
doAnswer(invocation -> { playerCallbacks.add(invocation.getArgument(1)); return null; })
.when(bukkitScheduler).runTask(eq(plugin), any(Runnable.class), any(Entity.class));
try (var bukkit = mockStatic(Bukkit.class)) {
World world = mock(World.class); when(world.getUID()).thenReturn(WORLD);
bukkit.when(() -> Bukkit.getWorld(WORLD)).thenReturn(world);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
var scheduler = new Scheduler(plugin); List<Villager> spawned = installSpawner(world, scheduler);
var registry = new VotingNPCRegistry(directory.resolve("npcs.json")); await(registry.initialLoad());
await(registry.create(definition(1)));
List<Player> opened = new ArrayList<>();
var manager = new VotingNPCManager(plugin, scheduler, registry, opened::add);
try {
manager.configure(true); scheduler.runNext(scheduler.global, "global"); scheduler.runNext(scheduler.regions, "region");
Player player = mock(Player.class); when(player.getUniqueId()).thenReturn(new UUID(0, 50));
when(player.isOnline()).thenReturn(true); when(player.getWorld()).thenReturn(world);
when(player.getLocation()).thenReturn(new Location(world, 8, 64, 9));
var cancelled = new PlayerInteractEntityEvent(player, spawned.getFirst(), EquipmentSlot.HAND); cancelled.setCancelled(true);
manager.interact(cancelled);
manager.interact(new PlayerInteractEntityEvent(player, spawned.getFirst(), EquipmentSlot.OFF_HAND));
assertTrue(playerCallbacks.isEmpty());
var click = new PlayerInteractEntityEvent(player, spawned.getFirst(), EquipmentSlot.HAND);
manager.interact(click); assertTrue(click.isCancelled());
assertEquals(1, playerCallbacks.size());
playerCallbacks.remove().run();
assertEquals(List.of(player), opened, "healthy main-hand selection opens on the player callback");
Player second = mock(Player.class); when(second.getUniqueId()).thenReturn(new UUID(0, 51));
when(second.isOnline()).thenReturn(true); when(second.getWorld()).thenReturn(world);
when(second.getLocation()).thenReturn(new Location(world, 8, 64, 9));
manager.interact(new PlayerInteractEntityEvent(second, spawned.getFirst(), EquipmentSlot.HAND));
playerCallbacks.remove().run();
assertEquals(List.of(player, second), opened, "one player's selection must not block another player");
Player third = mock(Player.class); when(third.getUniqueId()).thenReturn(new UUID(0, 52));
when(third.isOnline()).thenReturn(true); when(third.getWorld()).thenReturn(world);
when(third.getLocation()).thenReturn(new Location(world, 8, 64, 9));
manager.interact(new PlayerInteractEntityEvent(third, spawned.getFirst(), EquipmentSlot.HAND));
await(manager.remove("voting"));
playerCallbacks.remove().run(); assertEquals(List.of(player, second), opened);
} finally { manager.close(); scheduler.runNext(scheduler.entities, "entity"); assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS)); }
}
}
@Test void productionMenuEntryDelegatesToTheExistingUrlCommandRatherThanAnotherMenuOrRewardPath() {
var plugin = mock(VotingPluginMain.class);
var unrelated = mock(com.bencodez.advancedcore.api.command.CommandHandler.class);
var url = mock(com.bencodez.advancedcore.api.command.CommandHandler.class);
var later = mock(com.bencodez.advancedcore.api.command.CommandHandler.class);
var player = mock(Player.class);
when(plugin.getVoteCommand()).thenReturn(new ArrayList<>(List.of(unrelated, url, later)));
when(url.runCommand(eq(player), aryEq(new String[] { "URL" }))).thenReturn(true);
VotingNPCManager.openVotingSites(plugin, player);
verify(unrelated).runCommand(eq(player), aryEq(new String[] { "URL" }));
verify(url).runCommand(eq(player), aryEq(new String[] { "URL" }));
verifyNoInteractions(later);
verify(player, never()).performCommand(anyString());
}
private static Chunk chunk(World world) {
Chunk chunk = mock(Chunk.class); when(chunk.getWorld()).thenReturn(world); when(chunk.getX()).thenReturn(0); when(chunk.getZ()).thenReturn(0); return chunk;
}
@SuppressWarnings("unchecked")
private static List<Villager> installSpawner(World world, Scheduler scheduler) {
List<Villager> spawned = new ArrayList<>();
doAnswer(invocation -> {
assertEquals("region", scheduler.owner);
Villager entity = mock(Villager.class); when(entity.getUniqueId()).thenReturn(new UUID(2, spawned.size() + 1));
when(entity.isValid()).thenReturn(true);
Consumer<Villager> initializer = invocation.getArgument(2); initializer.accept(entity);
doAnswer(removal -> { assertEquals("entity", scheduler.owner); return null; }).when(entity).remove();
spawned.add(entity); return entity;
}).when(world).spawn(any(Location.class), eq(Villager.class), any(Consumer.class));
return spawned;
}
}
@@ -0,0 +1,237 @@
package com.bencodez.votingplugin.npc;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class VotingNPCRegistryTest {
@TempDir Path directory;
private VotingNPCDefinition definition(int id) {
return new VotingNPCDefinition("npc-" + id, new UUID(0, id), new UUID(1, 0),
0.5, 64, 0.5, 0, 0, "Vote");
}
private static <T> T await(CompletionStage<T> stage) throws Exception {
return stage.toCompletableFuture().get(5, TimeUnit.SECONDS);
}
private static void close(VotingNPCRegistry registry) throws InterruptedException {
registry.close();
assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS));
}
private static void waitOn(CountDownLatch latch) throws IOException {
try {
if (!latch.await(5, TimeUnit.SECONDS)) throw new IOException("Synthetic worker latch timed out");
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IOException("Synthetic worker interrupted", interrupted);
}
}
@Test void linkedDirectoryAliasesShareTheSameExclusiveOwnerAndLeafLockLinksAreRejected() throws Exception {
Path real = Files.createDirectory(directory.resolve("real"));
Path linked = Files.createSymbolicLink(directory.resolve("linked"), real);
Path file = linked.resolve("npcs.json");
var first = new VotingNPCRegistry(file);
try {
await(first.initialLoad()); await(first.create(definition(1)));
var alias = new VotingNPCRegistry(real.resolve("npcs.json"));
try { assertThrows(ExecutionException.class, () -> await(alias.initialLoad())); }
finally { close(alias); }
} finally { close(first); }
var recovered = new VotingNPCRegistry(file);
try { assertEquals(List.of(definition(1)), await(recovered.initialLoad())); }
finally { close(recovered); }
Path lock = linked.resolve("other.json.lock");
Path unrelated = real.resolve("unrelated"); Files.writeString(unrelated, "unchanged");
Files.createSymbolicLink(lock, unrelated);
var rejected = new VotingNPCRegistry(linked.resolve("other.json"));
try { assertThrows(ExecutionException.class, () -> await(rejected.initialLoad())); }
finally { close(rejected); }
assertEquals("unchanged", Files.readString(unrelated));
}
@Test void orderedDefinitionsAreDurableAndDuplicateRequestsDoNotOverwrite() throws Exception {
Path file = directory.resolve("npcs.json");
var registry = new VotingNPCRegistry(file);
try {
assertEquals(List.of(), await(registry.initialLoad()));
var first = registry.create(definition(1));
var duplicate = registry.create(definition(1));
var second = registry.create(definition(2));
assertEquals(definition(1), await(first));
assertInstanceOf(IllegalArgumentException.class,
assertThrows(ExecutionException.class, () -> await(duplicate)).getCause());
await(second);
assertEquals(List.of(definition(1), definition(2)), new VotingNPCStore(file).load());
assertTrue(await(registry.remove("npc-1")));
assertFalse(await(registry.remove("npc-1")));
} finally { close(registry); }
var restarted = new VotingNPCRegistry(file);
try { assertEquals(List.of(definition(2)), await(restarted.initialLoad())); }
finally { close(restarted); }
}
@Test void failedReloadRetainsLastSnapshotButCannotPublishUntilRepaired() throws Exception {
Path file = directory.resolve("npcs.json");
var registry = new VotingNPCRegistry(file);
try {
await(registry.initialLoad()); await(registry.create(definition(1)));
byte[] healthy = Files.readAllBytes(file);
Files.writeString(file, "{invalid");
assertThrows(ExecutionException.class, () -> await(registry.reload()));
assertFalse(registry.available());
assertEquals(List.of(definition(1)), registry.snapshot());
assertThrows(ExecutionException.class, () -> await(registry.create(definition(2))));
assertEquals("{invalid", Files.readString(file));
Files.write(file, healthy);
assertEquals(List.of(definition(1)), await(registry.reload()));
await(registry.create(definition(2)));
} finally { close(registry); }
}
@Test void invalidInitialFileDoesNotAppearReadyOrGetReplaced() throws Exception {
Path file = directory.resolve("npcs.json"); Files.writeString(file, "{invalid");
var registry = new VotingNPCRegistry(file);
try {
assertThrows(ExecutionException.class, () -> await(registry.initialLoad()));
assertFalse(registry.available());
assertThrows(ExecutionException.class, () -> await(registry.create(definition(1))));
assertEquals("{invalid", Files.readString(file));
} finally { close(registry); }
}
@Test void closeReturnsWhileAcceptedWriteDrainsAndKeepsExclusiveFileOwnership() throws Exception {
Path file = directory.resolve("npcs.json");
CountDownLatch entered = new CountDownLatch(1), release = new CountDownLatch(1);
var store = new VotingNPCStore(file) {
@Override synchronized void save(List<VotingNPCDefinition> values) throws IOException {
entered.countDown(); waitOn(release); super.save(values);
}
};
var first = new VotingNPCRegistry(file, store);
VotingNPCRegistry replacement = null;
try {
await(first.initialLoad());
var writing = first.create(definition(1));
assertTrue(entered.await(5, TimeUnit.SECONDS));
first.close(); first.close();
assertFalse(first.awaitClosed(0, TimeUnit.SECONDS));
assertThrows(ExecutionException.class, () -> await(first.remove("npc-1")));
replacement = new VotingNPCRegistry(file);
var next = replacement;
assertThrows(ExecutionException.class, () -> await(next.initialLoad()));
assertFalse(replacement.available());
release.countDown(); await(writing);
assertTrue(first.awaitClosed(5, TimeUnit.SECONDS));
assertEquals(List.of(definition(1)), await(replacement.reload()));
} finally {
release.countDown(); close(first);
if (replacement != null) close(replacement);
}
}
@Test void saturatedReloadIsRejectedWithoutDisablingTheHealthyOwner() throws Exception {
Path file = directory.resolve("npcs.json");
CountDownLatch entered = new CountDownLatch(1), release = new CountDownLatch(1);
var store = new VotingNPCStore(file) {
@Override synchronized void save(List<VotingNPCDefinition> values) throws IOException {
entered.countDown(); waitOn(release); super.save(values);
}
};
var registry = new VotingNPCRegistry(file, store);
try {
await(registry.initialLoad());
var writing = registry.create(definition(1));
assertTrue(entered.await(5, TimeUnit.SECONDS));
var queued = new java.util.ArrayList<CompletionStage<Boolean>>();
for (int i = 0; i < 128; i++) queued.add(registry.remove("missing"));
assertThrows(ExecutionException.class, () -> await(registry.reload()));
assertTrue(registry.available(), "rejected reload must leave existing readiness intact");
release.countDown(); await(writing);
for (var entry : queued) assertFalse(await(entry));
await(registry.create(definition(2)));
} finally { release.countDown(); close(registry); }
}
@Test void failedWriteFencesAlreadyQueuedMutationsUntilCheckedReload() throws Exception {
Path file = directory.resolve("npcs.json");
CountDownLatch entered = new CountDownLatch(1), release = new CountDownLatch(1);
AtomicBoolean fail = new AtomicBoolean(true);
var store = new VotingNPCStore(file) {
@Override synchronized void save(List<VotingNPCDefinition> values) throws IOException {
if (fail.getAndSet(false)) {
entered.countDown(); waitOn(release);
super.save(values);
throw new IOException("Synthetic failure after visible write");
}
super.save(values);
}
};
var registry = new VotingNPCRegistry(file, store);
try {
await(registry.initialLoad());
var first = registry.create(definition(1));
assertTrue(entered.await(5, TimeUnit.SECONDS));
var second = registry.create(definition(2));
release.countDown();
assertThrows(ExecutionException.class, () -> await(first));
assertThrows(ExecutionException.class, () -> await(second));
assertFalse(registry.available());
assertEquals(List.of(), registry.snapshot());
assertEquals(List.of(definition(1)), new VotingNPCStore(file).load());
assertEquals(List.of(definition(1)), await(registry.reload()));
await(registry.create(definition(2)));
assertEquals(List.of(definition(1), definition(2)), registry.snapshot());
} finally { release.countDown(); close(registry); }
}
@Test void completionCallbackMayCloseWithoutDeadlockingOrInterruptingCommittedWork() throws Exception {
var registry = new VotingNPCRegistry(directory.resolve("npcs.json"));
try {
await(registry.initialLoad());
CompletableFuture<Void> callback = registry.create(definition(1)).thenAccept(value -> registry.close())
.toCompletableFuture();
await(callback);
assertTrue(registry.awaitClosed(5, TimeUnit.SECONDS));
assertEquals(List.of(definition(1)), new VotingNPCStore(directory.resolve("npcs.json")).load());
} finally { close(registry); }
}
@Test void capacityAndIdentityRejectionsLeaveHealthyStorageAvailable() throws Exception {
Path file = directory.resolve("npcs.json");
var seed = new VotingNPCStore(file); seed.load();
seed.save(java.util.stream.IntStream.rangeClosed(1, 63).mapToObj(this::definition).toList());
var registry = new VotingNPCRegistry(file);
try {
await(registry.initialLoad());
byte[] original = Files.readAllBytes(file);
var duplicateIdentity = new VotingNPCDefinition("another", definition(1).identity(), definition(1).world(),
0, 64, 0, 0, 0, "Vote");
assertThrows(ExecutionException.class, () -> await(registry.create(duplicateIdentity)));
assertTrue(registry.available(), "invalid new identity must not disable existing NPC storage");
assertArrayEquals(original, Files.readAllBytes(file));
await(registry.create(definition(64)));
byte[] atLimit = Files.readAllBytes(file);
assertThrows(ExecutionException.class, () -> await(registry.create(definition(65))));
assertTrue(registry.available(), "one extra creation must not disable 64 existing NPCs");
assertEquals(64, registry.snapshot().size());
assertArrayEquals(atLimit, Files.readAllBytes(file));
} finally { close(registry); }
}
}
@@ -0,0 +1,78 @@
package com.bencodez.votingplugin.npc;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.junit.jupiter.api.Test;
import com.bencodez.votingplugin.VotingPluginMain;
class VotingNPCSchedulerTest {
@Test void alreadyOwnedCleanupRunsInlineEvenDuringDisable() {
var plugin = mock(VotingPluginMain.class);
try (var bukkit = mockStatic(Bukkit.class)) {
bukkit.when(Bukkit::isPrimaryThread).thenReturn(true);
var scheduler = new VotingNPCScheduler(plugin); var ran = new AtomicInteger();
scheduler.entity(mock(Entity.class), ran::incrementAndGet, () -> fail("retired"), error -> fail(error));
assertEquals(1, ran.get()); verify(plugin, never()).getBukkitScheduler();
}
}
@Test void deferredRegionActionUsesTheLocationSchedulerAndReportsFailure() {
var plugin = mock(VotingPluginMain.class, RETURNS_DEEP_STUBS);
try (var bukkit = mockStatic(Bukkit.class)) {
var scheduler = new VotingNPCScheduler(plugin); Location anchor = mock(Location.class);
var completion = new CompletableFuture<Void>();
var callback = new AtomicReference<Runnable>();
when(plugin.getBukkitScheduler().getFoliaLib().getImpl().runAtLocation(eq(anchor), any()))
.thenAnswer(inv -> { Consumer<?> action = inv.getArgument(1); callback.set(() -> action.accept(null)); return completion; });
var ran = new AtomicInteger(); List<Throwable> failures = new ArrayList<>();
scheduler.region(anchor, ran::incrementAndGet, failures::add);
assertEquals(0, ran.get()); callback.get().run(); assertEquals(1, ran.get());
completion.completeExceptionally(new IllegalStateException("Synthetic scheduler failure"));
assertEquals(1, failures.size());
}
}
@Test @SuppressWarnings({"unchecked", "rawtypes"})
void schedulerRetirementDoesNotPretendThatAnEntityWasRemoved() throws Exception {
var plugin = mock(VotingPluginMain.class, RETURNS_DEEP_STUBS);
try (var bukkit = mockStatic(Bukkit.class)) {
var scheduler = new VotingNPCScheduler(plugin); Entity entity = mock(Entity.class);
CompletableFuture completion = new CompletableFuture();
when(plugin.getBukkitScheduler().getFoliaLib().getImpl().runAtEntityWithFallback(eq(entity), any(), any()))
.thenReturn(completion);
var ran = new AtomicInteger(); var retired = new AtomicInteger(); List<Throwable> failures = new ArrayList<>();
scheduler.entity(entity, ran::incrementAndGet, retired::incrementAndGet, failures::add);
completion.complete(Enum.valueOf((Class) Class.forName("com.bencodez.simpleapi.folialib.enums.EntityTaskResult"), "SCHEDULER_RETIRED"));
assertEquals(0, ran.get()); assertEquals(0, retired.get()); assertEquals(1, failures.size());
}
}
@Test @SuppressWarnings({"unchecked", "rawtypes"})
void entityRetirementFallbackAndFutureCompletionAreHandledOnce() throws Exception {
var plugin = mock(VotingPluginMain.class, RETURNS_DEEP_STUBS);
try (var bukkit = mockStatic(Bukkit.class)) {
var scheduler = new VotingNPCScheduler(plugin); Entity entity = mock(Entity.class);
CompletableFuture completion = new CompletableFuture(); var fallback = new AtomicReference<Runnable>();
when(plugin.getBukkitScheduler().getFoliaLib().getImpl().runAtEntityWithFallback(eq(entity), any(), any()))
.thenAnswer(inv -> { fallback.set(inv.getArgument(2)); return completion; });
var retired = new AtomicInteger();
scheduler.entity(entity, () -> fail("Entity already retired"), retired::incrementAndGet, error -> fail(error));
fallback.get().run();
completion.complete(Enum.valueOf((Class) Class.forName("com.bencodez.simpleapi.folialib.enums.EntityTaskResult"), "ENTITY_RETIRED"));
assertEquals(1, retired.get());
}
}
}
@@ -0,0 +1,158 @@
package com.bencodez.votingplugin.npc;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.IntStream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class VotingNPCStoreTest {
@TempDir Path directory;
private VotingNPCDefinition definition(int id) {
return new VotingNPCDefinition("npc-" + id, new UUID(0, id), new UUID(1, 0),
-0.5, 64, 16.5, 90, 0, "Vote");
}
@Test void roundTripPreservesStableIdentitiesAndPositionAcrossRestart() throws Exception {
Path file = directory.resolve("npcs.json");
var store = new VotingNPCStore(file);
assertEquals(List.of(), store.load());
store.save(List.of(definition(1), definition(2)));
assertEquals(List.of(definition(1), definition(2)), new VotingNPCStore(file).load());
assertEquals(-1, definition(1).chunkX());
assertEquals(1, definition(1).chunkZ());
}
@Test void missingInitialReadDoesNotCreateFilesAndUninitializedStoreCannotOverwrite() throws Exception {
Path file = directory.resolve("npcs.json");
var store = new VotingNPCStore(file);
assertThrows(IOException.class, () -> store.save(List.of(definition(1))));
assertFalse(Files.exists(file));
assertTrue(store.load().isEmpty());
assertFalse(Files.exists(file));
}
@Test void invalidLoadKeepsOriginalBytesAndBlocksMutations() throws Exception {
Path file = directory.resolve("npcs.json");
byte[] original = "{ damaged definitions".getBytes(StandardCharsets.UTF_8);
Files.write(file, original);
var store = new VotingNPCStore(file);
assertThrows(IOException.class, store::load);
assertThrows(IOException.class, () -> store.save(List.of()));
assertArrayEquals(original, Files.readAllBytes(file));
}
@Test void duplicateIdentitiesAndCapacityRejectionDoNotChangeDurableDefinitions() throws Exception {
Path file = directory.resolve("npcs.json");
var store = new VotingNPCStore(file); store.load(); store.save(List.of(definition(1)));
byte[] before = Files.readAllBytes(file);
assertThrows(IOException.class, () -> store.save(List.of(definition(1), definition(1))));
var sameIdentity = new VotingNPCDefinition("another", definition(1).identity(), definition(1).world(),
0, 64, 0, 0, 0, "Vote");
assertThrows(IOException.class, () -> store.save(List.of(definition(1), sameIdentity)));
assertThrows(IOException.class, () -> store.save(IntStream.rangeClosed(1, 65).mapToObj(this::definition).toList()));
assertArrayEquals(before, Files.readAllBytes(file));
}
@Test void publicationFailureFencesOldStateUntilCheckedReload() throws Exception {
Path file = directory.resolve("npcs.json");
AtomicBoolean fail = new AtomicBoolean();
var store = new VotingNPCStore(file) {
@Override void forceDirectory(Path parent) throws IOException {
if (fail.get()) throw new IOException("Synthetic directory sync failure");
super.forceDirectory(parent);
}
};
store.load(); store.save(List.of(definition(1)));
fail.set(true);
assertThrows(IOException.class, () -> store.save(List.of(definition(1), definition(2))));
byte[] uncertain = Files.readAllBytes(file);
fail.set(false);
assertThrows(IOException.class, () -> store.save(List.of(definition(1))));
assertArrayEquals(uncertain, Files.readAllBytes(file));
assertEquals(List.of(definition(1), definition(2)), store.load());
store.save(List.of(definition(2)));
assertEquals(List.of(definition(2)), new VotingNPCStore(file).load());
}
@Test void missingUncertainFileDoesNotBecomeAnEmptySuccessfulRecovery() throws Exception {
Path file = directory.resolve("npcs.json");
var store = new VotingNPCStore(file) {
@Override void forceDirectory(Path ignored) throws IOException { throw new IOException("Synthetic failure"); }
};
store.load(); assertThrows(IOException.class, () -> store.save(List.of(definition(1))));
Files.delete(file);
assertThrows(IOException.class, store::load);
assertThrows(IOException.class, () -> store.save(List.of()));
assertFalse(Files.exists(file));
}
@Test void strictFieldsAndTypesRejectMalformedDocumentsIndividually() throws Exception {
Path file = directory.resolve("npcs.json");
var store = new VotingNPCStore(file); store.load(); store.save(List.of(definition(1)));
String healthy = Files.readString(file);
for (String damaged : List.of(
healthy.replace("\"version\": 1", "\"version\": 1, \"version\": 1"),
healthy.replace("\"version\": 1", "\"version\": \"1\""),
healthy.replace("\"version\": 1", "\"version\": 1.0"),
healthy.replace("\"id\": \"npc-1\"", "\"id\": \"npc-1\", \"id\": \"other\""),
healthy.replace("\"x\": -0.5", "\"x\": \"-0.5\""),
healthy.replace("\"name\": \"Vote\"", "\"unknown\": \"Vote\""),
healthy.replace("\"name\": \"Vote\"", "\"name\": null"),
healthy.replace("\"yaw\": 90.0", "\"yaw\": 1e100"),
healthy + "{}")) {
Files.writeString(file, damaged);
assertThrows(IOException.class, () -> new VotingNPCStore(file).load(), damaged);
assertEquals(damaged, Files.readString(file));
}
}
@Test void invalidUtf8OversizeAndSymlinkCannotBeLoadedOrOverwritten() throws Exception {
Path file = directory.resolve("npcs.json");
Files.write(file, new byte[] {(byte) 0xc3, (byte) 0x28});
assertThrows(IOException.class, () -> new VotingNPCStore(file).load());
Files.write(file, new byte[128 * 1024 + 1]);
assertThrows(IOException.class, () -> new VotingNPCStore(file).load());
Files.delete(file);
Path target = directory.resolve("unrelated.json");
Files.writeString(target, "unrelated");
Files.createSymbolicLink(file, target);
assertThrows(IOException.class, () -> new VotingNPCStore(file).load());
assertEquals("unrelated", Files.readString(target));
}
@Test void linkedDirectoriesAllowDurableDefinitionsButLeafLinksStayRejected() throws Exception {
Path real = Files.createDirectory(directory.resolve("real"));
Path linked = Files.createSymbolicLink(directory.resolve("linked"), real);
Path nested = Files.createDirectory(real.resolve("data"));
for (Path parent : List.of(linked, linked.resolve("data"))) {
Path file = parent.resolve("npcs.json");
var store = new VotingNPCStore(file);
assertTrue(store.load().isEmpty());
store.save(List.of(definition(1)));
assertEquals(List.of(definition(1)), new VotingNPCStore(file).load());
}
Path leaf = linked.resolve("alias.json");
Files.createSymbolicLink(leaf, nested.resolve("npcs.json"));
assertThrows(IOException.class, () -> new VotingNPCStore(leaf).load());
assertEquals(List.of(definition(1)), new VotingNPCStore(nested.resolve("npcs.json")).load());
}
@Test void invalidDefinitionsAreRejectedBeforePublication() {
assertThrows(IllegalArgumentException.class, () -> new VotingNPCDefinition("bad id", new UUID(0, 1),
new UUID(1, 0), 0, 64, 0, 0, 0, "Vote"));
assertThrows(IllegalArgumentException.class, () -> new VotingNPCDefinition("valid", new UUID(0, 1),
new UUID(1, 0), Double.NaN, 64, 0, 0, 0, "Vote"));
assertThrows(IllegalArgumentException.class, () -> new VotingNPCDefinition("valid", new UUID(0, 1),
new UUID(1, 0), 0, 64, 0, 0, 0, "Vote\nNew line"));
}
}
@@ -1,4 +1,4 @@
package com.bencodez.votingplugin.tests.packaging;
package com.bencodez.votingplugin.packaging;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -112,10 +112,9 @@ public class PackagedArtifactTest {
URL platformSlf4j = org.slf4j.Logger.class.getProtectionDomain().getCodeSource().getLocation();
try (URLClassLoader loader = new URLClassLoader(new URL[] { jar, platformSlf4j },
ClassLoader.getPlatformClassLoader())) {
Class<?> redisClient = Class.forName("redis.clients.jedis.RedisClient", true, loader);
try (AutoCloseable client = (AutoCloseable) redisClient.getMethod("create").invoke(null)) {
assertTrue(Class.forName("redis.clients.jedis.UnifiedJedis", true, loader).isInstance(client));
}
Class<?> unifiedJedis = Class.forName("redis.clients.jedis.UnifiedJedis", true, loader);
Object client = unifiedJedis.getConstructor().newInstance();
unifiedJedis.getMethod("close").invoke(client);
}
}
@@ -1,10 +1,6 @@
package com.bencodez.votingplugin.tests.placeholders;
package com.bencodez.votingplugin.placeholders;
import com.bencodez.votingplugin.placeholders.PlaceHolders;
import com.bencodez.votingplugin.placeholders.PlaceholderCacheLevel;
import com.bencodez.votingplugin.placeholders.PlaceholderPlayerPresence;
import com.bencodez.votingplugin.tests.util.EntityTaskResultTestCompat;
import com.bencodez.votingplugin.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;
@@ -57,11 +53,11 @@ class PlaceHoldersWorkerSafetyTest {
fixture.publish(placeholder);
try (var bukkit = mockStatic(Bukkit.class)) {
PlaceHoldersAccess.onUserDataChange(fixture.placeholders, fixture.advancedUser, "Points");
fixture.placeholders.onUserDataChange(fixture.advancedUser, "Points");
assertEquals(0, requests.get(), "offline online-only users must not be refreshed");
fixture.presence.playerOnline(fixture.player);
PlaceHoldersAccess.onUserDataChange(fixture.placeholders, fixture.advancedUser, "Points");
fixture.placeholders.onUserDataChange(fixture.advancedUser, "Points");
assertEquals(1, requests.get());
assertEquals("value", placeholder.getCache().get("points").get(fixture.uuid));
verify(fixture.scheduler, never()).runTask(eq(fixture.plugin), any(Runnable.class));
@@ -70,7 +66,7 @@ class PlaceHoldersWorkerSafetyTest {
fixture.presence.playerOffline(fixture.uuid);
fixture.placeholders.onLogout(fixture.uuid);
assertFalse(placeholder.getCache().get("points").containsKey(fixture.uuid));
PlaceHoldersAccess.onUserDataChange(fixture.placeholders, fixture.advancedUser, "Points");
fixture.placeholders.onUserDataChange(fixture.advancedUser, "Points");
assertEquals(1, requests.get());
bukkit.verifyNoInteractions();
}
@@ -82,10 +78,10 @@ class PlaceHoldersWorkerSafetyTest {
fixture.presence.playerOnline(fixture.player);
AtomicInteger requests = new AtomicInteger();
PlaceHolder<VotingPluginUser> placeholder = fixture.cachedPlaceholder("LastVotes", requests);
fixture.placeholders.getPlaceholders().add(PlaceHoldersAccess.platformOwned(fixture.placeholders, placeholder));
PlaceHoldersAccess.publishUserDataChangePlaceholders(fixture.placeholders);
fixture.placeholders.getPlaceholders().add(fixture.placeholders.platformOwned(placeholder));
fixture.placeholders.publishUserDataChangePlaceholders();
PlaceHoldersAccess.onUserDataChange(fixture.placeholders, fixture.advancedUser, "LastVotes");
fixture.placeholders.onUserDataChange(fixture.advancedUser, "LastVotes");
assertEquals(0, requests.get());
ArgumentCaptor<Runnable> task = ArgumentCaptor.forClass(Runnable.class);
@@ -101,8 +97,8 @@ class PlaceHoldersWorkerSafetyTest {
fixture.presence.playerOnline(fixture.player);
AtomicInteger requests = new AtomicInteger();
PlaceHolder<VotingPluginUser> placeholder = fixture.cachedPlaceholder("LastVotes", requests);
fixture.placeholders.getPlaceholders().add(PlaceHoldersAccess.platformOwned(fixture.placeholders, placeholder));
PlaceHoldersAccess.publishUserDataChangePlaceholders(fixture.placeholders);
fixture.placeholders.getPlaceholders().add(fixture.placeholders.platformOwned(placeholder));
fixture.placeholders.publishUserDataChangePlaceholders();
fixture.placeholders.onUpdate(fixture.votingUser, true);
@@ -143,7 +139,7 @@ class PlaceHoldersWorkerSafetyTest {
when(fixture.userManager.getVotingPluginUser(fixture.uuid)).thenReturn(fixture.votingUser);
fixture.presence.playerOnline(fixture.uuid, onlinePlayer);
assertEquals(fixture.votingUser, PlaceHoldersAccess.resolvePlaceholderUser(fixture.placeholders, onlinePlayer));
assertEquals(fixture.votingUser, fixture.placeholders.resolvePlaceholderUser(onlinePlayer));
verify(fixture.userManager).getVotingPluginUser(fixture.uuid);
verify(fixture.userManager, never()).getVotingPluginUser(onlinePlayer);
@@ -164,8 +160,8 @@ class PlaceHoldersWorkerSafetyTest {
"CanVoteSites", "LastVotes", requests);
placeholder.setUseCache(true, "CanVoteSites");
placeholder.getCache().get("CanVoteSites").put(fixture.uuid, "old");
fixture.placeholders.getPlaceholders().add(PlaceHoldersAccess.platformOwned(fixture.placeholders, placeholder));
PlaceHoldersAccess.publishUserDataChangePlaceholders(fixture.placeholders);
fixture.placeholders.getPlaceholders().add(fixture.placeholders.platformOwned(placeholder));
fixture.placeholders.publishUserDataChangePlaceholders();
fixture.placeholders.onUpdate(fixture.votingUser, false);
@@ -183,7 +179,7 @@ class PlaceHoldersWorkerSafetyTest {
fixture.presence.playerOnline(fixture.player);
AtomicInteger requests = new AtomicInteger();
PlaceHolder<VotingPluginUser> placeholder = fixture.cachedPlaceholder("LastVotes", requests);
fixture.placeholders.getPlaceholders().add(PlaceHoldersAccess.platformOwned(fixture.placeholders, placeholder));
fixture.placeholders.getPlaceholders().add(fixture.placeholders.platformOwned(placeholder));
fixture.placeholders.onUpdate(fixture.votingUser, true);
@@ -200,12 +196,12 @@ class PlaceHoldersWorkerSafetyTest {
PlaceHolder<VotingPluginUser> available = fixture.cachedPlaceholder("SitesAvailable", "LastVotes", liveRequests);
PlaceHolder<VotingPluginUser> total = fixture.cachedPlaceholder("SitesAvailableTotal", "LastVotes", liveRequests);
fixture.placeholders.getPlaceholders().add(
PlaceHoldersAccess.platformOwnedWithOfflineWorkerFallback(fixture.placeholders, available));
fixture.placeholders.platformOwnedWithOfflineWorkerFallback(available));
fixture.placeholders.getPlaceholders().add(
PlaceHoldersAccess.platformOwnedWithOfflineWorkerFallback(fixture.placeholders, total));
PlaceHoldersAccess.publishUserDataChangePlaceholders(fixture.placeholders);
fixture.placeholders.platformOwnedWithOfflineWorkerFallback(total));
fixture.placeholders.publishUserDataChangePlaceholders();
PlaceHoldersAccess.onUserDataChange(fixture.placeholders, fixture.advancedUser, "LastVotes");
fixture.placeholders.onUserDataChange(fixture.advancedUser, "LastVotes");
assertEquals("4", available.getCache().get("sitesavailable").get(fixture.uuid));
assertEquals("6", total.getCache().get("sitesavailabletotal").get(fixture.uuid));
@@ -221,8 +217,8 @@ class PlaceHoldersWorkerSafetyTest {
PlaceHolder<VotingPluginUser> available = fixture.cachedPlaceholder(
"SitesAvailable", "LastVotes", liveRequests);
fixture.placeholders.getPlaceholders().add(
PlaceHoldersAccess.platformOwnedWithOfflineWorkerFallback(fixture.placeholders, available));
PlaceHoldersAccess.publishUserDataChangePlaceholders(fixture.placeholders);
fixture.placeholders.platformOwnedWithOfflineWorkerFallback(available));
fixture.placeholders.publishUserDataChangePlaceholders();
fixture.presence.playerOnline(fixture.player);
FoliaLib folia = mock(FoliaLib.class);
ServerImplementation entityScheduler = mock(ServerImplementation.class);
@@ -236,7 +232,7 @@ class PlaceHoldersWorkerSafetyTest {
doAnswer(call -> { call.getArgument(1, Runnable.class).run(); return null; })
.when(fixture.scheduler).runTask(eq(fixture.plugin), any(Runnable.class));
PlaceHoldersAccess.onUserDataChange(fixture.placeholders, fixture.advancedUser, "LastVotes");
fixture.placeholders.onUserDataChange(fixture.advancedUser, "LastVotes");
assertEquals("4", available.getCache().get("sitesavailable").get(fixture.uuid));
assertEquals(0, liveRequests.get(), "retirement fallback must not read live player permissions");
@@ -250,11 +246,11 @@ class PlaceHoldersWorkerSafetyTest {
PlaceHolder<VotingPluginUser> available = fixture.cachedPlaceholder(
"SitesAvailable", "LastVotes", liveRequests);
fixture.placeholders.getPlaceholders().add(
PlaceHoldersAccess.platformOwnedWithOfflineWorkerFallback(fixture.placeholders, available));
PlaceHoldersAccess.publishUserDataChangePlaceholders(fixture.placeholders);
fixture.placeholders.platformOwnedWithOfflineWorkerFallback(available));
fixture.placeholders.publishUserDataChangePlaceholders();
fixture.presence.playerOnline(fixture.player);
PlaceHoldersAccess.onUserDataChange(fixture.placeholders, fixture.advancedUser, "LastVotes");
fixture.placeholders.onUserDataChange(fixture.advancedUser, "LastVotes");
ArgumentCaptor<Runnable> task = ArgumentCaptor.forClass(Runnable.class);
verify(fixture.scheduler).runTask(eq(fixture.plugin), task.capture(), eq(fixture.player));
fixture.presence.playerOffline(fixture.uuid);
@@ -271,10 +267,10 @@ class PlaceHoldersWorkerSafetyTest {
fixture.presence.playerOnline(fixture.player);
AtomicInteger requests = new AtomicInteger();
PlaceHolder<VotingPluginUser> placeholder = fixture.cachedPlaceholder("LastVotes", requests);
fixture.placeholders.getPlaceholders().add(PlaceHoldersAccess.platformOwned(fixture.placeholders, placeholder));
PlaceHoldersAccess.publishUserDataChangePlaceholders(fixture.placeholders);
fixture.placeholders.getPlaceholders().add(fixture.placeholders.platformOwned(placeholder));
fixture.placeholders.publishUserDataChangePlaceholders();
PlaceHoldersAccess.onUserDataChange(fixture.placeholders, fixture.advancedUser, "LastVotes");
fixture.placeholders.onUserDataChange(fixture.advancedUser, "LastVotes");
ArgumentCaptor<Runnable> tasks = ArgumentCaptor.forClass(Runnable.class);
verify(fixture.scheduler).runTask(eq(fixture.plugin), tasks.capture(), eq(fixture.player));
@@ -305,13 +301,13 @@ class PlaceHoldersWorkerSafetyTest {
AtomicInteger requests = new AtomicInteger();
PlaceHolder<VotingPluginUser> placeholder = fixture.cachedPlaceholder(
"CanVoteSites", "LastVotes", requests);
fixture.placeholders.getPlaceholders().add(PlaceHoldersAccess.platformOwned(fixture.placeholders, placeholder));
PlaceHoldersAccess.publishUserDataChangePlaceholders(fixture.placeholders);
fixture.placeholders.getPlaceholders().add(fixture.placeholders.platformOwned(placeholder));
fixture.placeholders.publishUserDataChangePlaceholders();
PlaceHoldersAccess.onUserDataChange(fixture.placeholders, fixture.advancedUser, "LastVotes");
fixture.placeholders.onUserDataChange(fixture.advancedUser, "LastVotes");
fixture.presence.playerOffline(fixture.uuid);
fixture.presence.playerOnline(replacement);
PlaceHoldersAccess.onUserDataChange(fixture.placeholders, fixture.advancedUser, "LastVotes");
fixture.placeholders.onUserDataChange(fixture.advancedUser, "LastVotes");
ArgumentCaptor<Runnable> tasks = ArgumentCaptor.forClass(Runnable.class);
verify(fixture.scheduler, times(2)).runTask(eq(fixture.plugin), tasks.capture(), any(Player.class));
@@ -333,12 +329,12 @@ class PlaceHoldersWorkerSafetyTest {
"Changed", "LastVotes", changedRequests);
PlaceHolder<VotingPluginUser> warmupOnly = fixture.cachedPlaceholder(
"WarmupOnly", "Unrelated", warmupRequests);
fixture.placeholders.getPlaceholders().add(PlaceHoldersAccess.platformOwned(fixture.placeholders, changed));
fixture.placeholders.getPlaceholders().add(PlaceHoldersAccess.platformOwned(fixture.placeholders, warmupOnly));
PlaceHoldersAccess.publishUserDataChangePlaceholders(fixture.placeholders);
fixture.placeholders.getPlaceholders().add(fixture.placeholders.platformOwned(changed));
fixture.placeholders.getPlaceholders().add(fixture.placeholders.platformOwned(warmupOnly));
fixture.placeholders.publishUserDataChangePlaceholders();
fixture.placeholders.onUpdate(fixture.votingUser, true);
PlaceHoldersAccess.onUserDataChange(fixture.placeholders, fixture.advancedUser, "LastVotes");
fixture.placeholders.onUserDataChange(fixture.advancedUser, "LastVotes");
ArgumentCaptor<Runnable> tasks = ArgumentCaptor.forClass(Runnable.class);
verify(fixture.scheduler, times(2)).runTask(eq(fixture.plugin), tasks.capture(), eq(fixture.player));
@@ -371,7 +367,7 @@ class PlaceHoldersWorkerSafetyTest {
boolean bulkUpdate = i % 2 == 0;
updates.add(() -> {
if (bulkUpdate) fixture.placeholders.onUpdate(fixture.votingUser, true);
else PlaceHoldersAccess.onUserDataChange(fixture.placeholders, fixture.advancedUser, "LastVotes");
else fixture.placeholders.onUserDataChange(fixture.advancedUser, "LastVotes");
return null;
});
}
@@ -452,7 +448,7 @@ class PlaceHoldersWorkerSafetyTest {
void publish(PlaceHolder<VotingPluginUser> placeholder) {
placeholders.getPlaceholders().add(placeholder);
PlaceHoldersAccess.publishUserDataChangePlaceholders(placeholders);
placeholders.publishUserDataChangePlaceholders();
}
}
}
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.placeholders;
import com.bencodez.votingplugin.placeholders.PlaceholderPlayerPresence;
package com.bencodez.votingplugin.placeholders;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -1,7 +1,4 @@
package com.bencodez.votingplugin.tests.presets;
import com.bencodez.votingplugin.presets.GitHubVoteSitePresetLoader;
import com.bencodez.votingplugin.presets.VoteSitePresetSetupHandler;
package com.bencodez.votingplugin.presets;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -32,17 +29,17 @@ class VoteSitePresetSafetyTest {
+ "]";
assertEquals(Collections.singletonList("presets/votesites/crafty-gg.meta.json"),
GitHubVoteSitePresetLoaderAccess.parsePresetPaths(json.getBytes(StandardCharsets.UTF_8)));
assertFalse(GitHubVoteSitePresetLoaderAccess.isPresetPathAllowed("presets/votesites/../secret.meta.json"));
GitHubVoteSitePresetLoader.parsePresetPaths(json.getBytes(StandardCharsets.UTF_8)));
assertFalse(GitHubVoteSitePresetLoader.isPresetPathAllowed("presets/votesites/../secret.meta.json"));
StringBuilder tooMany = new StringBuilder("[");
for (int i = 0; i <= GitHubVoteSitePresetLoaderAccess.MAX_PRESET_COUNT; i++) {
for (int i = 0; i <= GitHubVoteSitePresetLoader.MAX_PRESET_COUNT; i++) {
if (i > 0) tooMany.append(',');
tooMany.append("{\"type\":\"file\",\"path\":\"presets/votesites/site-")
.append(i).append(".meta.json\"}");
}
tooMany.append(']');
assertThrows(IOException.class, () -> GitHubVoteSitePresetLoaderAccess
assertThrows(IOException.class, () -> GitHubVoteSitePresetLoader
.parsePresetPaths(tooMany.toString().getBytes(StandardCharsets.UTF_8)));
}
@@ -55,7 +52,7 @@ class VoteSitePresetSafetyTest {
when(plugin.getBukkitScheduler()).thenReturn(scheduler);
when(loader.listAllVoteSitePresets()).thenReturn(Collections.emptyList());
VoteSitePresetSetupHandlerAccess.create(plugin, loader).startSetup(player);
new VoteSitePresetSetupHandler(plugin, loader).startSetup(player);
verify(loader).listAllVoteSitePresets();
verify(scheduler).runTask(eq(plugin), any(Runnable.class), eq(player));
@@ -1,7 +1,4 @@
package com.bencodez.votingplugin.tests.proxy;
import com.bencodez.votingplugin.proxy.PendingIncomingVoteJournal;
import com.bencodez.votingplugin.proxy.VotingPluginWire;
package com.bencodez.votingplugin.proxy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.proxy;
import com.bencodez.votingplugin.tests.proxy.PluginMessageDispatcherAccess;
package com.bencodez.votingplugin.proxy;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
@@ -41,14 +39,14 @@ class PluginMessageDispatcherTest {
if (!wake.future.isCancelled()) { when(wake.future.isDone()).thenReturn(true); wake.action.run(); }
}
}
PluginMessageDispatcherAccess dispatcher(java.util.function.Consumer<String> replay) {
return new PluginMessageDispatcherAccess(clock::get, () -> scheduler, replay);
PluginMessageDispatcher dispatcher(java.util.function.Consumer<String> replay) {
return new PluginMessageDispatcher(clock::get, () -> scheduler, replay);
}
}
@Test void expiredMetadataReleasesCapacityWithoutRetiringEffectBearingControls() {
Harness h = new Harness();
PluginMessageDispatcherAccess d = h.dispatcher(server -> { });
PluginMessageDispatcher d = h.dispatcher(server -> { });
assertTrue(d.enqueue("backend-a", 1, 0, false, () -> false));
assertTrue(d.enqueue("backend-a", 1, 0, true, () -> false));
h.tick(20);
@@ -63,7 +61,7 @@ class PluginMessageDispatcherTest {
@Test void cheapBudgetCheckDoesNotReserveOrBypassTheActualAttempt() {
Harness h = new Harness();
PluginMessageDispatcherAccess d = h.dispatcher(server -> { });
PluginMessageDispatcher d = h.dispatcher(server -> { });
assertTrue(d.hasBudget("backend-a", "vote-a", false));
assertTrue(d.hasBudget("backend-a", "vote-a", false));
assertTrue(d.trySend("backend-a", "vote-a", false, () -> true));
@@ -78,7 +76,7 @@ class PluginMessageDispatcherTest {
@Test void concurrentProducersShareOneBackendBudget() throws Exception {
Harness h = new Harness();
PluginMessageDispatcherAccess d = h.dispatcher(server -> { });
PluginMessageDispatcher d = h.dispatcher(server -> { });
CountDownLatch gate = new CountDownLatch(1);
AtomicInteger accepted = new AtomicInteger();
List<Thread> workers = new ArrayList<>();
@@ -102,7 +100,7 @@ class PluginMessageDispatcherTest {
@Test void immediateAndDelayedControlsDoNotCatchUpAfterStall() {
Harness h = new Harness();
PluginMessageDispatcherAccess d = h.dispatcher(server -> { });
PluginMessageDispatcher d = h.dispatcher(server -> { });
List<Long> attempts = new ArrayList<>();
assertTrue(d.trySend("backend-a", () -> { attempts.add(h.clock.get()); return true; }));
for (int i = 0; i < 5; i++) assertTrue(d.enqueue("backend-a", 1, 0, () -> {
@@ -115,12 +113,12 @@ class PluginMessageDispatcherTest {
h.tick(1); assertTrue(attempts.size() <= afterStall + 1);
for (int i = 0; i < 10; i++) h.tick(20);
assertEquals(6, attempts.size());
for (int i = 1; i < attempts.size(); i++) assertTrue(attempts.get(i) - attempts.get(i - 1) >= PluginMessageDispatcherAccess.INTERVAL);
for (int i = 1; i < attempts.size(); i++) assertTrue(attempts.get(i) - attempts.get(i - 1) >= PluginMessageDispatcher.INTERVAL);
}
@Test void failedCarrierIsRetainedAndDoesNotBlockOtherBackend() {
Harness h = new Harness();
PluginMessageDispatcherAccess d = h.dispatcher(server -> { });
PluginMessageDispatcher d = h.dispatcher(server -> { });
AtomicInteger a = new AtomicInteger(), b = new AtomicInteger();
assertTrue(d.enqueue("backend-a", 4, 0, () -> a.incrementAndGet() > 1));
assertTrue(d.enqueue("backend-b", 4, 0, () -> { b.incrementAndGet(); return true; }));
@@ -131,7 +129,7 @@ class PluginMessageDispatcherTest {
@Test void delayedControlDoesNotBlockReadyMessageAndIdenticalMessagesRemainDistinct() {
Harness h = new Harness();
PluginMessageDispatcherAccess d = h.dispatcher(server -> { });
PluginMessageDispatcher d = h.dispatcher(server -> { });
List<String> seen = new ArrayList<>();
d.enqueue("backend-a", 1, TimeUnit.SECONDS.toNanos(10), () -> { seen.add("delayed"); return true; });
d.enqueue("backend-a", 1, 0, () -> { seen.add("ready"); return true; });
@@ -143,8 +141,8 @@ class PluginMessageDispatcherTest {
@Test void queueCapacityAndSchedulerRejectionAreObservable() {
Harness h = new Harness();
PluginMessageDispatcherAccess d = h.dispatcher(server -> { });
for (int i = 0; i < PluginMessageDispatcherAccess.MAX_LANE_CONTROLS; i++) assertTrue(d.enqueue("backend-a", 1, 0, () -> false));
PluginMessageDispatcher d = h.dispatcher(server -> { });
for (int i = 0; i < PluginMessageDispatcher.MAX_LANE_CONTROLS; i++) assertTrue(d.enqueue("backend-a", 1, 0, () -> false));
assertFalse(d.enqueue("backend-a", 1, 0, () -> false));
assertTrue(d.enqueue("backend-b", 1, 0, () -> true));
d.close(); assertFalse(d.enqueue("backend-b", 1, 0, () -> true));
@@ -157,8 +155,8 @@ class PluginMessageDispatcherTest {
@Test void reliableWindowDoesNotPreventReceiptReleaseAndAcknowledgementCanBeReentrant() {
Harness h = new Harness();
PluginMessageDispatcherAccess d = h.dispatcher(server -> { });
for (int i = 0; i < PluginMessageDispatcherAccess.MAX_IN_FLIGHT; i++) {
PluginMessageDispatcher d = h.dispatcher(server -> { });
for (int i = 0; i < PluginMessageDispatcher.MAX_IN_FLIGHT; i++) {
assertTrue(d.trySend("backend-a", "vote-" + i, false, () -> true)); h.tick(20);
}
assertFalse(d.trySend("backend-a", "extra", false, () -> true));
@@ -175,15 +173,15 @@ class PluginMessageDispatcherTest {
@Test void closeInvalidatesScheduledCallbacksAndIsIdempotent() {
Harness h = new Harness();
AtomicInteger sends = new AtomicInteger();
PluginMessageDispatcherAccess d = h.dispatcher(server -> fail("closed owner callback"));
PluginMessageDispatcher d = h.dispatcher(server -> fail("closed owner callback"));
d.enqueue("backend-a", 1, 0, () -> { sends.incrementAndGet(); return true; });
d.close(); d.close(); h.tick(1000);
assertEquals(0, sends.get()); assertFalse(d.trySend("backend-a", () -> true));
}
@Test void failedRetryKeepsPreviouslyAcceptedWindowOccupancy() {
Harness h = new Harness();
PluginMessageDispatcherAccess d = h.dispatcher(server -> { });
for (int i = 0; i < PluginMessageDispatcherAccess.MAX_IN_FLIGHT; i++) {
PluginMessageDispatcher d = h.dispatcher(server -> { });
for (int i = 0; i < PluginMessageDispatcher.MAX_IN_FLIGHT; i++) {
assertTrue(d.trySend("backend-a", "vote-" + i, false, () -> true)); h.tick(20);
}
h.tick(10000);
@@ -194,8 +192,8 @@ class PluginMessageDispatcherTest {
@Test void outstandingReleasesDoNotConsumeVoteWindow() {
Harness h = new Harness();
PluginMessageDispatcherAccess d = h.dispatcher(server -> { });
for (int i = 0; i < PluginMessageDispatcherAccess.MAX_IN_FLIGHT; i++) {
PluginMessageDispatcher d = h.dispatcher(server -> { });
for (int i = 0; i < PluginMessageDispatcher.MAX_IN_FLIGHT; i++) {
assertTrue(d.trySend("backend-a", "release-" + i, true, () -> true)); h.tick(20);
}
assertTrue(d.trySend("backend-a", "new-vote", false, () -> true));
@@ -203,7 +201,7 @@ class PluginMessageDispatcherTest {
@Test void fullWindowWaitsForRetryDeadlineInsteadOfRescanningEveryInterval() {
Harness h = new Harness();
PluginMessageDispatcherAccess[] dispatcher = new PluginMessageDispatcherAccess[1];
PluginMessageDispatcher[] dispatcher = new PluginMessageDispatcher[1];
AtomicInteger replays = new AtomicInteger();
dispatcher[0] = h.dispatcher(server -> {
replays.incrementAndGet();
@@ -217,20 +215,20 @@ class PluginMessageDispatcherTest {
@Test void batchReservationDoesNotSendBeforeCommitAndCancellationReclaimsCapacity() {
Harness h = new Harness();
PluginMessageDispatcherAccess d = h.dispatcher(server -> { });
PluginMessageDispatcher d = h.dispatcher(server -> { });
AtomicInteger sends = new AtomicInteger();
PluginMessageDispatcherAccess.Batch batch = d.reserveBatch(java.util.Map.of("backend-a",
PluginMessageDispatcher.Batch batch = d.reserveBatch(java.util.Map.of("backend-a",
() -> { sends.incrementAndGet(); return true; }), 4);
assertNotNull(batch); h.tick(20); assertEquals(0, sends.get());
batch.cancel(); h.tick(1000); assertEquals(0, sends.get());
PluginMessageDispatcherAccess.Batch committed = d.reserveBatch(java.util.Map.of("backend-a",
PluginMessageDispatcher.Batch committed = d.reserveBatch(java.util.Map.of("backend-a",
() -> { sends.incrementAndGet(); return true; }), 4);
assertNotNull(committed); committed.activate(); h.tick(20); assertEquals(1, sends.get());
}
@Test void aBlockedBackendDoesNotHoldTheGlobalDispatcherMonitor() throws Exception {
Harness h = new Harness();
PluginMessageDispatcherAccess d = h.dispatcher(server -> { });
PluginMessageDispatcher d = h.dispatcher(server -> { });
CountDownLatch entered = new CountDownLatch(1), release = new CountDownLatch(1);
Thread blocked = new Thread(() -> d.trySend("backend-a", () -> {
entered.countDown();
@@ -1,7 +1,4 @@
package com.bencodez.votingplugin.tests.proxy;
import com.bencodez.votingplugin.proxy.ProxyRuntimeReplacementLifecycle;
import com.bencodez.votingplugin.proxy.VotingPluginProxy;
package com.bencodez.votingplugin.proxy;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -1,7 +1,4 @@
package com.bencodez.votingplugin.tests.proxy;
import com.bencodez.votingplugin.proxy.VotingPluginProxy;
import com.bencodez.votingplugin.proxy.VotingPluginProxyConfig;
package com.bencodez.votingplugin.proxy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -21,7 +18,7 @@ class RedisClientConfigTest {
when(config.getRedisDbIndex()).thenReturn(2);
when(config.getRedisSsl()).thenReturn(true);
DefaultJedisClientConfig clientConfig = VotingPluginProxyAccess.buildRedisClientConfig(config);
DefaultJedisClientConfig clientConfig = VotingPluginProxy.buildRedisClientConfig(config);
assertTrue(clientConfig.isSsl());
assertEquals(2, clientConfig.getDatabase());
@@ -32,6 +29,6 @@ class RedisClientConfigTest {
void tlsRemainsDisabledByDefault() {
VotingPluginProxyConfig config = mock(VotingPluginProxyConfig.class);
assertFalse(VotingPluginProxyAccess.buildRedisClientConfig(config).isSsl());
assertFalse(VotingPluginProxy.buildRedisClientConfig(config).isSsl());
}
}
@@ -1,7 +1,4 @@
package com.bencodez.votingplugin.tests.proxy;
import com.bencodez.votingplugin.tests.proxy.ReliableVoteDeliveryOutboxAccess;
import com.bencodez.votingplugin.proxy.VotingPluginWire;
package com.bencodez.votingplugin.proxy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -26,8 +23,8 @@ class ReliableVoteDeliveryOutboxTest {
@Test
void updateQuotaDoesNotConsumeTheRewardEntryAllowance() throws Exception {
Path file = directory.resolve("updates.dat");
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(file);
for (int i = 0; i < ReliableVoteDeliveryOutboxAccess.MAX_UPDATE_ENTRIES; i++) {
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(file);
for (int i = 0; i < ReliableVoteDeliveryOutbox.MAX_UPDATE_ENTRIES; i++) {
JsonEnvelope update = VotingPluginWire.voteUpdate(new UUID(1, i).toString(), 1, 10,
"synthetic-service", 1L, "").toBuilder()
.put(VotingPluginWire.K_VOTE_ID, new UUID(2, i)).build();
@@ -39,8 +36,8 @@ class ReliableVoteDeliveryOutboxTest {
assertFalse(outbox.offer("backend-a", extra));
assertTrue(outbox.offer("backend-a", VotingPluginWire.vote("test-player", new UUID(1, 4096).toString(),
"synthetic-service", 1L, true, true, "", new UUID(2, 4096), false, false, 1, 1)));
assertEquals(ReliableVoteDeliveryOutboxAccess.MAX_UPDATE_ENTRIES + 1,
new ReliableVoteDeliveryOutboxAccess(file).size());
assertEquals(ReliableVoteDeliveryOutbox.MAX_UPDATE_ENTRIES + 1,
new ReliableVoteDeliveryOutbox(file).size());
}
@Test
@@ -49,19 +46,19 @@ class ReliableVoteDeliveryOutboxTest {
UUID voteId = UUID.randomUUID();
JsonEnvelope vote = VotingPluginWire.vote("Player", UUID.randomUUID().toString(), "site", 10L,
true, true, "", voteId, false, false, 1, 1);
ReliableVoteDeliveryOutboxAccess first = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox first = new ReliableVoteDeliveryOutbox(file);
assertTrue(first.offer("Survival", vote));
assertEquals(1, first.size());
assertTrue(Files.isRegularFile(file));
ReliableVoteDeliveryOutboxAccess restarted = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox restarted = new ReliableVoteDeliveryOutbox(file);
assertEquals(1, restarted.size());
assertFalse(restarted.acknowledgeCompletion("creative", voteId, VotingPluginWire.SUB_VOTE));
assertFalse(restarted.acknowledgeCompletion("survival", voteId, VotingPluginWire.SUB_VOTE_ONLINE));
assertTrue(restarted.acknowledgeCompletion("survival", voteId, VotingPluginWire.SUB_VOTE));
assertEquals(1, restarted.size());
assertTrue(new ReliableVoteDeliveryOutboxAccess(file).snapshot().get(0).awaitingReceiptRelease());
assertTrue(new ReliableVoteDeliveryOutbox(file).snapshot().get(0).awaitingReceiptRelease());
assertTrue(restarted.acknowledgeReceiptRelease("survival", voteId, VotingPluginWire.SUB_VOTE));
assertEquals(0, restarted.size());
assertFalse(Files.exists(file));
@@ -72,7 +69,7 @@ class ReliableVoteDeliveryOutboxTest {
UUID voteId = UUID.randomUUID();
JsonEnvelope vote = VotingPluginWire.voteOnline("Player", UUID.randomUUID().toString(), "site", 10L,
true, true, "", voteId, false, false, 1, 1);
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat"));
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat"));
assertTrue(outbox.offer("Survival", vote));
assertTrue(outbox.offer("survival", vote));
@@ -87,10 +84,10 @@ class ReliableVoteDeliveryOutboxTest {
UUID voteId = UUID.randomUUID();
JsonEnvelope rejection = VotingPluginWire.voteDelayRejected(
"Player", UUID.randomUUID().toString(), "site", true, voteId);
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(file);
assertTrue(outbox.offer("survival", rejection));
ReliableVoteDeliveryOutboxAccess restarted = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox restarted = new ReliableVoteDeliveryOutbox(file);
assertEquals(1, restarted.size());
assertEquals(voteId.toString(), restarted.snapshot().get(0).envelope().getFields()
.get(VotingPluginWire.K_VOTE_ID));
@@ -98,7 +95,7 @@ class ReliableVoteDeliveryOutboxTest {
"survival", voteId, VotingPluginWire.SUB_VOTE_DELAY_REJECTED));
assertTrue(restarted.acknowledgeReceiptRelease(
"survival", voteId, VotingPluginWire.SUB_VOTE_DELAY_REJECTED));
assertEquals(0, new ReliableVoteDeliveryOutboxAccess(file).size());
assertEquals(0, new ReliableVoteDeliveryOutbox(file).size());
}
@Test
@@ -107,25 +104,25 @@ class ReliableVoteDeliveryOutboxTest {
UUID fencedId = UUID.randomUUID();
JsonEnvelope fenced = VotingPluginWire.voteDelayRejected(
"Player", UUID.randomUUID().toString(), "site", true, fencedId);
ReliableVoteDeliveryOutboxAccess fencedOutbox = new ReliableVoteDeliveryOutboxAccess(fencedFile);
ReliableVoteDeliveryOutbox fencedOutbox = new ReliableVoteDeliveryOutbox(fencedFile);
assertTrue(fencedOutbox.offer("survival", fenced));
assertTrue(fencedOutbox.beginLegacyDelivery(
"survival", fencedId, VotingPluginWire.SUB_VOTE_DELAY_REJECTED));
assertTrue(new ReliableVoteDeliveryOutboxAccess(fencedFile).retireLegacyDelivery(
assertTrue(new ReliableVoteDeliveryOutbox(fencedFile).retireLegacyDelivery(
"survival", fencedId, VotingPluginWire.SUB_VOTE_DELAY_REJECTED));
assertEquals(0, new ReliableVoteDeliveryOutboxAccess(fencedFile).size());
assertEquals(0, new ReliableVoteDeliveryOutbox(fencedFile).size());
Path completedFile = directory.resolve("completed.dat");
UUID completedId = UUID.randomUUID();
JsonEnvelope completed = VotingPluginWire.voteDelayRejected(
"Player", UUID.randomUUID().toString(), "site", true, completedId);
ReliableVoteDeliveryOutboxAccess completedOutbox = new ReliableVoteDeliveryOutboxAccess(completedFile);
ReliableVoteDeliveryOutbox completedOutbox = new ReliableVoteDeliveryOutbox(completedFile);
assertTrue(completedOutbox.offer("survival", completed));
assertTrue(completedOutbox.acknowledgeCompletion(
"survival", completedId, VotingPluginWire.SUB_VOTE_DELAY_REJECTED));
assertTrue(new ReliableVoteDeliveryOutboxAccess(completedFile).retireLegacyDelivery(
assertTrue(new ReliableVoteDeliveryOutbox(completedFile).retireLegacyDelivery(
"survival", completedId, VotingPluginWire.SUB_VOTE_DELAY_REJECTED));
assertEquals(0, new ReliableVoteDeliveryOutboxAccess(completedFile).size());
assertEquals(0, new ReliableVoteDeliveryOutbox(completedFile).size());
}
@Test
@@ -133,7 +130,7 @@ class ReliableVoteDeliveryOutboxTest {
Path file = directory.resolve("outbox.dat");
UUID firstId = UUID.randomUUID();
UUID secondId = UUID.randomUUID();
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(file);
assertTrue(outbox.offer("survival", VotingPluginWire.vote("One", UUID.randomUUID().toString(),
"site", 10L, true, true, "", firstId, false, false, 1, 1)));
assertTrue(outbox.offer("survival", VotingPluginWire.vote("Two", UUID.randomUUID().toString(),
@@ -141,7 +138,7 @@ class ReliableVoteDeliveryOutboxTest {
assertTrue(outbox.acknowledgeCompletion("survival", firstId, VotingPluginWire.SUB_VOTE));
assertTrue(outbox.acknowledgeReceiptRelease("survival", firstId, VotingPluginWire.SUB_VOTE));
ReliableVoteDeliveryOutboxAccess restarted = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox restarted = new ReliableVoteDeliveryOutbox(file);
assertEquals(1, restarted.size());
assertEquals(secondId.toString(), restarted.snapshot().get(0).envelope().getFields()
.get(VotingPluginWire.K_VOTE_ID));
@@ -150,29 +147,29 @@ class ReliableVoteDeliveryOutboxTest {
@Test
void restartIgnoresOnlyATruncatedFinalAppend() throws Exception {
Path file = directory.resolve("outbox.dat");
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(file);
assertTrue(outbox.offer("survival", VotingPluginWire.vote("One", UUID.randomUUID().toString(),
"site", 10L, true, true, "", UUID.randomUUID(), false, false, 1, 1)));
Files.writeString(file, "R\tc3Vydml2YWw", StandardOpenOption.APPEND);
ReliableVoteDeliveryOutboxAccess repaired = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox repaired = new ReliableVoteDeliveryOutbox(file);
assertEquals(1, repaired.size());
assertTrue(repaired.offer("survival", VotingPluginWire.vote("Two", UUID.randomUUID().toString(),
"site", 11L, true, true, "", UUID.randomUUID(), false, false, 1, 1)));
assertEquals(2, new ReliableVoteDeliveryOutboxAccess(file).size());
assertEquals(2, new ReliableVoteDeliveryOutbox(file).size());
}
@Test
void repairsPartialAppendBeforeSameProcessRetry() throws Exception {
Path file = directory.resolve("outbox.dat");
UUID voteId = UUID.randomUUID();
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(file);
assertTrue(outbox.offer("survival", VotingPluginWire.vote("One", UUID.randomUUID().toString(),
"site", 10L, true, true, "", voteId, false, false, 1, 1)));
Files.writeString(file, "C\tpartial", StandardOpenOption.APPEND);
assertTrue(outbox.acknowledgeCompletion("survival", voteId, VotingPluginWire.SUB_VOTE));
ReliableVoteDeliveryOutboxAccess restarted = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox restarted = new ReliableVoteDeliveryOutbox(file);
assertEquals(1, restarted.size());
assertTrue(restarted.snapshot().get(0).awaitingReceiptRelease());
}
@@ -180,7 +177,7 @@ class ReliableVoteDeliveryOutboxTest {
@Test
void acceptedVotesReserveCapacityForCompletionAndRemovalRecords() throws Exception {
Path file = directory.resolve("outbox.dat");
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(file);
List<UUID> accepted = new ArrayList<>();
String padding = "x".repeat(16 * 1024);
@@ -208,32 +205,32 @@ class ReliableVoteDeliveryOutboxTest {
Path file = directory.resolve("deferred.dat");
UUID id = new UUID(7, 1);
JsonEnvelope update = update(id);
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(file);
assertTrue(outbox.retainUpdate("backend-a", update, false));
assertTrue(outbox.snapshot().get(0).deferredUpdate());
assertFalse(outbox.beginLegacyDelivery("backend-a", id, VotingPluginWire.SUB_VOTE_UPDATE));
ReliableVoteDeliveryOutboxAccess restored = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox restored = new ReliableVoteDeliveryOutbox(file);
assertTrue(restored.snapshot().get(0).deferredUpdate());
assertEquals(id.toString(), restored.snapshot().get(0).envelope().getFields().get(VotingPluginWire.K_VOTE_ID));
assertTrue(restored.setUpdateReady("backend-a", id, true));
restored = new ReliableVoteDeliveryOutboxAccess(file);
restored = new ReliableVoteDeliveryOutbox(file);
assertFalse(restored.snapshot().get(0).deferredUpdate());
assertTrue(restored.beginLegacyDelivery("backend-a", id, VotingPluginWire.SUB_VOTE_UPDATE));
assertTrue(restored.retireLegacyDelivery("backend-a", id, VotingPluginWire.SUB_VOTE_UPDATE));
assertEquals(0, new ReliableVoteDeliveryOutboxAccess(file).size());
assertEquals(0, new ReliableVoteDeliveryOutbox(file).size());
}
@Test
void rejectedReadyAttemptCanReturnToDeferredOwnershipAndRecoverWithoutAnAmbiguousFence() throws Exception {
Path file = directory.resolve("rejected-update.dat");
UUID id = new UUID(7, 2);
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(file);
assertTrue(outbox.offer("backend-a", update(id)));
assertTrue(outbox.beginLegacyDelivery("backend-a", id, VotingPluginWire.SUB_VOTE_UPDATE));
assertFalse(outbox.setUpdateReady("backend-a", id, false));
assertTrue(outbox.rejectLegacyDelivery("backend-a", id, VotingPluginWire.SUB_VOTE_UPDATE));
assertTrue(outbox.setUpdateReady("backend-a", id, false));
ReliableVoteDeliveryOutboxAccess restored = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox restored = new ReliableVoteDeliveryOutbox(file);
assertTrue(restored.snapshot().get(0).deferredUpdate());
assertFalse(restored.snapshot().get(0).legacyDeliveryFenced());
assertFalse(restored.snapshot().get(0).legacyDeliveryRejected());
@@ -243,7 +240,7 @@ class ReliableVoteDeliveryOutboxTest {
@Test
void deferredUpdatesRetainOrderAndDoNotAdmitARewardThroughTheDeferredApi() throws Exception {
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(directory.resolve("ordered.dat"));
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(directory.resolve("ordered.dat"));
UUID first = new UUID(7, 3), second = new UUID(7, 4);
assertTrue(outbox.retainUpdate("backend-a", update(first), false));
assertTrue(outbox.retainUpdate("backend-a", update(second), true));
@@ -261,7 +258,7 @@ class ReliableVoteDeliveryOutboxTest {
void failedDeferredPublicationDoesNotReportDurableAdmission() throws Exception {
Path blocked = directory.resolve("blocked");
Files.writeString(blocked, "synthetic");
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(blocked.resolve("updates.dat"));
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(blocked.resolve("updates.dat"));
assertFalse(outbox.retainUpdate("backend-a", update(new UUID(7, 5)), false));
assertEquals(0, outbox.size());
}
@@ -269,20 +266,20 @@ class ReliableVoteDeliveryOutboxTest {
@Test
void deferredCapacityIsBoundedAndDoesNotEvictExistingOwnersOrRewardAllowance() throws Exception {
Path file = directory.resolve("deferred-capacity.dat");
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(file);
for (int i = 0; i < ReliableVoteDeliveryOutboxAccess.MAX_DEFERRED_UPDATE_ENTRIES; i++) {
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(file);
for (int i = 0; i < ReliableVoteDeliveryOutbox.MAX_DEFERRED_UPDATE_ENTRIES; i++) {
assertTrue(outbox.retainUpdate("backend-a", update(new UUID(8, i)), false));
}
UUID overflow = new UUID(8, ReliableVoteDeliveryOutboxAccess.MAX_DEFERRED_UPDATE_ENTRIES);
UUID overflow = new UUID(8, ReliableVoteDeliveryOutbox.MAX_DEFERRED_UPDATE_ENTRIES);
assertFalse(outbox.retainUpdate("backend-a", update(overflow), false));
assertTrue(outbox.retainUpdate("backend-a", update(new UUID(8, 0)), false));
assertTrue(outbox.offer("backend-b", VotingPluginWire.vote("test-player",
new UUID(1, 1).toString(), "synthetic-service", 1L, true, true, "", overflow,
false, false, 1, 1)));
ReliableVoteDeliveryOutboxAccess restored = new ReliableVoteDeliveryOutboxAccess(file);
assertEquals(ReliableVoteDeliveryOutboxAccess.MAX_DEFERRED_UPDATE_ENTRIES + 1, restored.size());
assertEquals(ReliableVoteDeliveryOutboxAccess.MAX_DEFERRED_UPDATE_ENTRIES,
restored.snapshot().stream().filter(ReliableVoteDeliveryOutboxAccess.Entry::deferredUpdate).count());
ReliableVoteDeliveryOutbox restored = new ReliableVoteDeliveryOutbox(file);
assertEquals(ReliableVoteDeliveryOutbox.MAX_DEFERRED_UPDATE_ENTRIES + 1, restored.size());
assertEquals(ReliableVoteDeliveryOutbox.MAX_DEFERRED_UPDATE_ENTRIES,
restored.snapshot().stream().filter(ReliableVoteDeliveryOutbox.Entry::deferredUpdate).count());
assertFalse(restored.find("backend-a", overflow, VotingPluginWire.SUB_VOTE_UPDATE).isPresent());
}
@@ -1,10 +1,4 @@
package com.bencodez.votingplugin.tests.proxy;
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
import com.bencodez.votingplugin.proxy.PendingIncomingVote;
import com.bencodez.votingplugin.proxy.PendingIncomingVoteQueue;
import com.bencodez.votingplugin.proxy.VotingPluginProxy;
package com.bencodez.votingplugin.proxy;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
@@ -136,13 +130,13 @@ class ServiceSiteVoteLoggingRegressionTest {
java.lang.reflect.Method method = findMethod(plugin.getClass(), "processPendingIncomingVote",
PendingIncomingVote.class);
method.setAccessible(true);
ReflectiveAccess.invoke(method, plugin, new Object[]{pending});
method.invoke(plugin, pending);
}
private static void setField(Object target, String name, Object value) throws Exception {
java.lang.reflect.Field field = findField(target.getClass(), name);
field.setAccessible(true);
ReflectiveAccess.set(field, target, value);
field.set(target, value);
}
private static Object getField(Object target, String name) throws Exception {
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.proxy;
import com.bencodez.votingplugin.proxy.BungeeMethod;
package com.bencodez.votingplugin.proxy;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
@@ -1,11 +1,4 @@
package com.bencodez.votingplugin.tests.proxy;
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
import com.bencodez.votingplugin.proxy.BungeeMethod;
import com.bencodez.votingplugin.tests.proxy.ReliableVoteDeliveryOutboxAccess;
import com.bencodez.votingplugin.proxy.VotingPluginProxy;
import com.bencodez.votingplugin.proxy.VotingPluginWire;
package com.bencodez.votingplugin.proxy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@@ -71,17 +64,17 @@ class VotingPluginProxyLifecycleTest {
"updateReliableVoteDeliveryCapability", String.class, JsonEnvelope.class);
update.setAccessible(true);
ReflectiveAccess.invoke(update, proxy, new Object[]{"backend-a", VotingPluginWire.statusOkay("backend-a")});
update.invoke(proxy, "backend-a", VotingPluginWire.statusOkay("backend-a"));
@SuppressWarnings("unchecked")
Set<String> reliable = (Set<String>) field(proxy, "reliableVoteDeliveryServers");
assertFalse(reliable.contains("backend-a"));
ReflectiveAccess.invoke(update, proxy, new Object[]{"backend-a", VotingPluginWire.authenticatedSocketVoteDeliveryCapability(
VotingPluginWire.statusOkay("backend-a"))});
update.invoke(proxy, "backend-a", VotingPluginWire.authenticatedSocketVoteDeliveryCapability(
VotingPluginWire.statusOkay("backend-a")));
assertTrue(reliable.contains("backend-a"));
setField(proxy, "socketAuthenticators", Map.of());
ReflectiveAccess.invoke(update, proxy, new Object[]{"legacy-backend", VotingPluginWire.statusOkay("legacy-backend")});
update.invoke(proxy, "legacy-backend", VotingPluginWire.statusOkay("legacy-backend"));
assertTrue(reliable.contains("legacy-backend"));
}
@@ -96,14 +89,14 @@ class VotingPluginProxyLifecycleTest {
TransportEnvelopeEncryption.Domain.PROXY_BACKEND, true);
Field encryptionField = VotingPluginProxy.class.getDeclaredField("communicationEncryption");
encryptionField.setAccessible(true);
ReflectiveAccess.set(encryptionField, proxy, encryption);
encryptionField.set(proxy, encryption);
ClientHandler client = mock(ClientHandler.class);
Field handles = VotingPluginProxy.class.getDeclaredField("clientHandles");
handles.setAccessible(true);
ReflectiveAccess.set(handles, proxy, new HashMap<>(Map.of("lobby", client)));
handles.set(proxy, new HashMap<>(Map.of("lobby", client)));
JsonEnvelope original = JsonEnvelope.builder("Vote").put("player", "Alex").build();
assertTrue(VotingPluginProxyAccess.sendProxyBroadcastEnvelopeNow(proxy, "lobby", original));
assertTrue(proxy.sendProxyBroadcastEnvelopeNow("lobby", original));
ArgumentCaptor<JsonEnvelope> sent = ArgumentCaptor.forClass(JsonEnvelope.class);
verify(client).sendEnvelope(sent.capture());
@@ -133,14 +126,14 @@ class VotingPluginProxyLifecycleTest {
JsonEnvelope valid = authenticator.sign(encryption.encrypt(VotingPluginWire.statusOkay("backend-a")),
Domain.SOCKET_PROXY_BACKEND, "backend-a", "proxy1");
ReflectiveAccess.invoke(accept, proxy, new Object[]{valid});
accept.invoke(proxy, valid);
ArgumentCaptor<JsonEnvelope> delivered = ArgumentCaptor.forClass(JsonEnvelope.class);
verify(messages).onMessage(delivered.capture());
assertEquals("backend-a", delivered.getValue().getFields().get(VotingPluginWire.K_SERVER));
JsonEnvelope wrongIdentity = authenticator.sign(encryption.encrypt(VotingPluginWire.statusOkay("backend-b")),
Domain.SOCKET_PROXY_BACKEND, "backend-a", "proxy1");
ReflectiveAccess.invoke(accept, proxy, new Object[]{wrongIdentity});
accept.invoke(proxy, wrongIdentity);
verify(messages).onMessage(org.mockito.ArgumentMatchers.any(JsonEnvelope.class));
}
@@ -161,7 +154,7 @@ class VotingPluginProxyLifecycleTest {
Method accept = VotingPluginProxy.class.getDeclaredMethod("acceptSocketEnvelope", JsonEnvelope.class);
accept.setAccessible(true);
ReflectiveAccess.invoke(accept, proxy, new Object[]{encryption.encrypt(VotingPluginWire.statusOkay("backend-a"))});
accept.invoke(proxy, encryption.encrypt(VotingPluginWire.statusOkay("backend-a")));
verifyNoInteractions(messages);
}
@@ -174,11 +167,11 @@ class VotingPluginProxyLifecycleTest {
"0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.US_ASCII)));
Field authentication = VotingPluginProxy.class.getDeclaredField("sharedTransportAuthenticator");
authentication.setAccessible(true);
ReflectiveAccess.set(authentication, proxy, SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED));
authentication.set(proxy, SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.REQUIRED));
@SuppressWarnings("unchecked")
Consumer<JsonEnvelope> accepted = mock(Consumer.class);
VotingPluginProxyAccess.acceptSharedTransportEnvelope(((VotingPluginProxy) proxy), VotingPluginWire.login("Alex",
((VotingPluginProxy) proxy).acceptSharedTransportEnvelope(VotingPluginWire.login("Alex",
"00000000-0000-0000-0000-000000000001", "backend-a"), Domain.REDIS_PROXY_BACKEND,
"VotingPlugin", accepted);
@@ -195,7 +188,7 @@ class VotingPluginProxyLifecycleTest {
Mode.REQUIRED);
Field authentication = VotingPluginProxy.class.getDeclaredField("sharedTransportAuthenticator");
authentication.setAccessible(true);
ReflectiveAccess.set(authentication, proxy, authenticator);
authentication.set(proxy, authenticator);
Field securityLockField = VotingPluginProxy.class.getDeclaredField("transportSecurityLock");
securityLockField.setAccessible(true);
Object securityLock = securityLockField.get(proxy);
@@ -203,7 +196,7 @@ class VotingPluginProxyLifecycleTest {
JsonEnvelope signed = authenticator.sign(VotingPluginWire.status("backend-a"),
Domain.REDIS_PROXY_BACKEND, "backend-a", channel);
assertDoesNotThrow(() -> VotingPluginProxyAccess.acceptSharedTransportEnvelope(((VotingPluginProxy) proxy), signed,
assertDoesNotThrow(() -> ((VotingPluginProxy) proxy).acceptSharedTransportEnvelope(signed,
Domain.REDIS_PROXY_BACKEND, channel, ignored -> {
CountDownLatch acquired = new CountDownLatch(1);
Thread contender = new Thread(() -> {
@@ -241,7 +234,7 @@ class VotingPluginProxyLifecycleTest {
@SuppressWarnings("unchecked")
Consumer<JsonEnvelope> accepted = mock(Consumer.class);
VotingPluginProxyAccess.acceptSharedTransportEnvelope(((VotingPluginProxy) proxy), signed, Domain.REDIS_PROXY_BACKEND, channel,
((VotingPluginProxy) proxy).acceptSharedTransportEnvelope(signed, Domain.REDIS_PROXY_BACKEND, channel,
accepted);
ArgumentCaptor<JsonEnvelope> delivered = ArgumentCaptor.forClass(JsonEnvelope.class);
@@ -262,10 +255,10 @@ class VotingPluginProxyLifecycleTest {
when(proxy.getConfig().getCommunicationEncryption()).thenReturn(true);
Field authentication = VotingPluginProxy.class.getDeclaredField("sharedTransportAuthenticator");
authentication.setAccessible(true);
ReflectiveAccess.set(authentication, proxy, SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.COMPATIBILITY));
authentication.set(proxy, SharedTransportEnvelopeAuthenticator.load(keyFile, Mode.COMPATIBILITY));
Field encryption = VotingPluginProxy.class.getDeclaredField("communicationEncryption");
encryption.setAccessible(true);
ReflectiveAccess.set(encryption, proxy, TransportEnvelopeEncryption.load(keyFile,
encryption.set(proxy, TransportEnvelopeEncryption.load(keyFile,
TransportEnvelopeEncryption.Domain.PROXY_BACKEND, false));
proxy.reloadFromControl();
@@ -294,10 +287,10 @@ class VotingPluginProxyLifecycleTest {
assertTrue(original.verify(signed, Domain.REDIS_PROXY_BACKEND, channel).accepted());
Field authentication = VotingPluginProxy.class.getDeclaredField("sharedTransportAuthenticator");
authentication.setAccessible(true);
ReflectiveAccess.set(authentication, proxy, original);
authentication.set(proxy, original);
Field encryption = VotingPluginProxy.class.getDeclaredField("communicationEncryption");
encryption.setAccessible(true);
ReflectiveAccess.set(encryption, proxy, TransportEnvelopeEncryption.load(keyFile,
encryption.set(proxy, TransportEnvelopeEncryption.load(keyFile,
TransportEnvelopeEncryption.Domain.PROXY_BACKEND, true));
proxy.reloadFromControl();
@@ -350,7 +343,7 @@ class VotingPluginProxyLifecycleTest {
GlobalMessageProxyHandler messages = mock(GlobalMessageProxyHandler.class);
UUID voteId = UUID.randomUUID();
UUID playerId = UUID.randomUUID();
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat"));
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat"));
org.junit.jupiter.api.Assertions.assertTrue(outbox.offer("survival", VotingPluginWire.vote(
"Player", playerId.toString(), "site", 10L, true, true, "", voteId,
false, false, 1, 1)));
@@ -362,8 +355,8 @@ class VotingPluginProxyLifecycleTest {
Method completion = VotingPluginProxy.class.getDeclaredMethod(
"handleVoteDeliveryAcknowledgement", JsonEnvelope.class);
completion.setAccessible(true);
ReflectiveAccess.invoke(completion, proxy, new Object[]{VotingPluginWire.voteDeliveryAcknowledgement(
"survival", voteId, VotingPluginWire.SUB_VOTE)});
completion.invoke(proxy, VotingPluginWire.voteDeliveryAcknowledgement(
"survival", voteId, VotingPluginWire.SUB_VOTE));
assertEquals(1, outbox.size());
org.junit.jupiter.api.Assertions.assertTrue(outbox.snapshot().get(0).awaitingReceiptRelease());
@@ -377,8 +370,8 @@ class VotingPluginProxyLifecycleTest {
Method released = VotingPluginProxy.class.getDeclaredMethod(
"handleVoteDeliveryReceiptReleaseAcknowledgement", JsonEnvelope.class);
released.setAccessible(true);
ReflectiveAccess.invoke(released, proxy, new Object[]{VotingPluginWire.voteDeliveryReceiptReleaseAcknowledgement(
"survival", voteId, VotingPluginWire.SUB_VOTE)});
released.invoke(proxy, VotingPluginWire.voteDeliveryReceiptReleaseAcknowledgement(
"survival", voteId, VotingPluginWire.SUB_VOTE));
assertEquals(0, outbox.size());
}
@@ -391,32 +384,32 @@ class VotingPluginProxyLifecycleTest {
UUID voteId = UUID.randomUUID();
JsonEnvelope vote = VotingPluginWire.vote("Player", UUID.randomUUID().toString(), "site", 10L,
true, true, "", voteId, false, false, 1, 1);
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat"));
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat"));
org.junit.jupiter.api.Assertions.assertTrue(outbox.offer("survival", vote));
Field outboxField = VotingPluginProxy.class.getDeclaredField("reliableVoteDeliveryOutbox");
outboxField.setAccessible(true);
ReflectiveAccess.set(outboxField, proxy, outbox);
outboxField.set(proxy, outbox);
Field messagesField = VotingPluginProxy.class.getDeclaredField("globalMessageProxyHandler");
messagesField.setAccessible(true);
ReflectiveAccess.set(messagesField, proxy, messages);
messagesField.set(proxy, messages);
Field legacyServers = VotingPluginProxy.class.getDeclaredField("legacyVoteDeliveryServers");
legacyServers.setAccessible(true);
@SuppressWarnings("unchecked")
Set<String> legacy = (Set<String>) legacyServers.get(proxy);
Method retry = VotingPluginProxy.class.getDeclaredMethod("retryReliableVoteDeliveries", String.class);
retry.setAccessible(true);
ReflectiveAccess.invoke(retry, proxy, new Object[]{"survival"});
retry.invoke(proxy, "survival");
assertEquals(1, outbox.size());
org.mockito.Mockito.verifyNoInteractions(messages);
legacy.add("survival");
proxy.setPluginMessageDeliveryResult(false);
ReflectiveAccess.invoke(retry, proxy, new Object[]{"survival"});
retry.invoke(proxy, "survival");
assertEquals(1, outbox.size());
proxy.setPluginMessageDeliveryResult(true);
proxy.advancePluginMessageClock(1000);
ReflectiveAccess.invoke(retry, proxy, new Object[]{"survival"});
retry.invoke(proxy, "survival");
assertEquals(0, outbox.size());
}
@@ -431,7 +424,7 @@ class VotingPluginProxyLifecycleTest {
initializeEmptyVoteCache(proxy);
proxy.setMethod(BungeeMethod.PLUGINMESSAGING);
proxy.setPluginMessageDeliveryResult(false);
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(file);
org.junit.jupiter.api.Assertions.assertTrue(outbox.offer("survival", vote));
setField(proxy, "reliableVoteDeliveryOutbox", outbox);
@SuppressWarnings("unchecked")
@@ -440,7 +433,7 @@ class VotingPluginProxyLifecycleTest {
Method retry = VotingPluginProxy.class.getDeclaredMethod("retryReliableVoteDeliveries", String.class);
retry.setAccessible(true);
ReflectiveAccess.invoke(retry, proxy, new Object[]{"survival"});
retry.invoke(proxy, "survival");
assertEquals(1, proxy.getVoteEnvelopeDeliveryAttempts());
org.junit.jupiter.api.Assertions.assertTrue(outbox.snapshot().get(0).legacyDeliveryRejected());
@@ -448,13 +441,13 @@ class VotingPluginProxyLifecycleTest {
VotingPluginProxyTestImpl restartedProxy = new VotingPluginProxyTestImpl();
initializeEmptyVoteCache(restartedProxy);
restartedProxy.setMethod(BungeeMethod.PLUGINMESSAGING);
ReliableVoteDeliveryOutboxAccess restartedOutbox = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox restartedOutbox = new ReliableVoteDeliveryOutbox(file);
setField(restartedProxy, "reliableVoteDeliveryOutbox", restartedOutbox);
@SuppressWarnings("unchecked")
Set<String> restartedLegacy = (Set<String>) field(restartedProxy, "legacyVoteDeliveryServers");
restartedLegacy.add("survival");
ReflectiveAccess.invoke(retry, restartedProxy, new Object[]{"survival"});
retry.invoke(restartedProxy, "survival");
assertEquals(1, restartedProxy.getVoteEnvelopeDeliveryAttempts());
assertEquals(0, restartedOutbox.size());
@@ -469,7 +462,7 @@ class VotingPluginProxyLifecycleTest {
JsonEnvelope vote = VotingPluginWire.vote("Player", UUID.randomUUID().toString(), "site", 10L,
true, true, "", voteId, false, false, 1, 1);
Path file = directory.resolve("outbox.dat");
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(file);
org.junit.jupiter.api.Assertions.assertTrue(outbox.offer("survival", vote));
setField(proxy, "reliableVoteDeliveryOutbox", outbox);
@SuppressWarnings("unchecked")
@@ -489,8 +482,8 @@ class VotingPluginProxyLifecycleTest {
}
});
ReflectiveAccess.invoke(retry, proxy, new Object[]{"survival"});
ReflectiveAccess.invoke(retry, proxy, new Object[]{"survival"});
retry.invoke(proxy, "survival");
retry.invoke(proxy, "survival");
assertEquals(1, proxy.getVoteEnvelopeDeliveryAttempts());
org.junit.jupiter.api.Assertions.assertFalse(outbox.snapshot().get(0).awaitingReceiptRelease());
@@ -501,12 +494,12 @@ class VotingPluginProxyLifecycleTest {
VotingPluginProxyTestImpl restartedProxy = new VotingPluginProxyTestImpl();
initializeEmptyVoteCache(restartedProxy);
restartedProxy.setMethod(BungeeMethod.PLUGINMESSAGING);
ReliableVoteDeliveryOutboxAccess restartedOutbox = new ReliableVoteDeliveryOutboxAccess(file);
ReliableVoteDeliveryOutbox restartedOutbox = new ReliableVoteDeliveryOutbox(file);
setField(restartedProxy, "reliableVoteDeliveryOutbox", restartedOutbox);
@SuppressWarnings("unchecked")
Set<String> restartedLegacy = (Set<String>) field(restartedProxy, "legacyVoteDeliveryServers");
restartedLegacy.add("survival");
ReflectiveAccess.invoke(retry, restartedProxy, new Object[]{"survival"});
retry.invoke(restartedProxy, "survival");
assertEquals(0, restartedProxy.getVoteEnvelopeDeliveryAttempts());
assertEquals(0, restartedOutbox.size());
}
@@ -521,7 +514,7 @@ class VotingPluginProxyLifecycleTest {
private static void setField(Object target, String name, Object value) throws Exception {
Field field = VotingPluginProxy.class.getDeclaredField(name);
field.setAccessible(true);
ReflectiveAccess.set(field, target, value);
field.set(target, value);
}
private static Object field(Object target, String name) throws Exception {
@@ -538,11 +531,11 @@ class VotingPluginProxyLifecycleTest {
GlobalMessageProxyHandler messages = mock(GlobalMessageProxyHandler.class);
Field messagesField = VotingPluginProxy.class.getDeclaredField("globalMessageProxyHandler");
messagesField.setAccessible(true);
ReflectiveAccess.set(messagesField, proxy, messages);
messagesField.set(proxy, messages);
Method probe = VotingPluginProxy.class.getDeclaredMethod("probeReliableVoteDeliveryCapabilities");
probe.setAccessible(true);
ReflectiveAccess.invoke(probe, proxy, new Object[]{});
probe.invoke(proxy);
org.mockito.ArgumentCaptor<JsonEnvelope> envelope = org.mockito.ArgumentCaptor.forClass(JsonEnvelope.class);
verify(messages).sendMessage(org.mockito.ArgumentMatchers.eq("survival"),
@@ -558,10 +551,10 @@ class VotingPluginProxyLifecycleTest {
VotingPluginProxyTestImpl proxy = new VotingPluginProxyTestImpl();
proxy.setMethod(BungeeMethod.HTTP);
proxy.setVoteEnvelopeDeliveryResult(false);
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat"));
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat"));
Field outboxField = VotingPluginProxy.class.getDeclaredField("reliableVoteDeliveryOutbox");
outboxField.setAccessible(true);
ReflectiveAccess.set(outboxField, proxy, outbox);
outboxField.set(proxy, outbox);
Field reliableServers = VotingPluginProxy.class.getDeclaredField("reliableVoteDeliveryServers");
reliableServers.setAccessible(true);
@SuppressWarnings("unchecked")
@@ -583,7 +576,7 @@ class VotingPluginProxyLifecycleTest {
VotingPluginProxyTestImpl proxy = new VotingPluginProxyTestImpl();
proxy.setMethod(BungeeMethod.PLUGINMESSAGING);
GlobalMessageProxyHandler messages = mock(GlobalMessageProxyHandler.class);
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat"));
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat"));
setField(proxy, "globalMessageProxyHandler", messages);
setField(proxy, "reliableVoteDeliveryOutbox", outbox);
JsonEnvelope vote = VotingPluginWire.vote("Player", UUID.randomUUID().toString(), "site", 10L,
@@ -600,7 +593,7 @@ class VotingPluginProxyLifecycleTest {
VotingPluginProxyTestImpl proxy = new VotingPluginProxyTestImpl();
proxy.setMethod(BungeeMethod.PLUGINMESSAGING);
GlobalMessageProxyHandler messages = mock(GlobalMessageProxyHandler.class);
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat"));
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat"));
setField(proxy, "globalMessageProxyHandler", messages);
setField(proxy, "reliableVoteDeliveryOutbox", outbox);
@SuppressWarnings("unchecked")
@@ -622,7 +615,7 @@ class VotingPluginProxyLifecycleTest {
VotingPluginProxyTestImpl proxy = new VotingPluginProxyTestImpl();
proxy.setMethod(BungeeMethod.HTTP);
proxy.setVoteEnvelopeDeliveryResult(true);
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat"));
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat"));
setField(proxy, "reliableVoteDeliveryOutbox", outbox);
@SuppressWarnings("unchecked")
Set<String> reliable = (Set<String>) field(proxy, "reliableVoteDeliveryServers");
@@ -646,7 +639,7 @@ class VotingPluginProxyLifecycleTest {
initializeEmptyVoteCache(proxy);
proxy.setMethod(BungeeMethod.HTTP);
proxy.setVoteEnvelopeDeliveryResult(true);
ReliableVoteDeliveryOutboxAccess outbox = new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat"));
ReliableVoteDeliveryOutbox outbox = new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat"));
setField(proxy, "reliableVoteDeliveryOutbox", outbox);
@SuppressWarnings("unchecked")
Set<String> reliable = (Set<String>) field(proxy, "reliableVoteDeliveryServers");
@@ -658,15 +651,15 @@ class VotingPluginProxyLifecycleTest {
org.junit.jupiter.api.Assertions.assertTrue(proxy.sendVoteEnvelopeAcceptedForTest("survival", 1, vote));
Method retry = VotingPluginProxy.class.getDeclaredMethod("retryReliableVoteDeliveries", String.class);
retry.setAccessible(true);
ReflectiveAccess.invoke(retry, proxy, new Object[]{"survival"});
ReflectiveAccess.invoke(retry, proxy, new Object[]{"survival"});
retry.invoke(proxy, "survival");
retry.invoke(proxy, "survival");
Method completion = VotingPluginProxy.class.getDeclaredMethod(
"handleVoteDeliveryAcknowledgement", JsonEnvelope.class);
completion.setAccessible(true);
ReflectiveAccess.invoke(completion, proxy, new Object[]{VotingPluginWire.voteDeliveryAcknowledgement(
"survival", voteId, VotingPluginWire.SUB_VOTE)});
ReflectiveAccess.invoke(retry, proxy, new Object[]{"survival"});
completion.invoke(proxy, VotingPluginWire.voteDeliveryAcknowledgement(
"survival", voteId, VotingPluginWire.SUB_VOTE));
retry.invoke(proxy, "survival");
java.util.List<String> deliveryIds = proxy.getAttemptedVotePartyDeliveryIds();
assertEquals(5, deliveryIds.size());
@@ -683,11 +676,11 @@ class VotingPluginProxyLifecycleTest {
doThrow(new IllegalStateException("operation still running")).when(connector).close();
Field control = VotingPluginProxy.class.getDeclaredField("controlConnector");
control.setAccessible(true);
ReflectiveAccess.set(control, proxy, connector);
control.set(proxy, connector);
Method stop = VotingPluginProxy.class.getDeclaredMethod("stopControlServices", boolean.class);
stop.setAccessible(true);
assertThrows(java.lang.reflect.InvocationTargetException.class, () -> ReflectiveAccess.invoke(stop, proxy, new Object[]{true}));
assertThrows(java.lang.reflect.InvocationTargetException.class, () -> stop.invoke(proxy, true));
assertSame(connector, control.get(proxy));
}
@@ -699,11 +692,11 @@ class VotingPluginProxyLifecycleTest {
doThrow(new IllegalStateException("still running")).when(manager).closeAndWait();
Field hosted = VotingPluginProxy.class.getDeclaredField("hostedControlManager");
hosted.setAccessible(true);
ReflectiveAccess.set(hosted, proxy, manager);
hosted.set(proxy, manager);
Method stop = VotingPluginProxy.class.getDeclaredMethod("stopControlServices", boolean.class);
stop.setAccessible(true);
assertThrows(java.lang.reflect.InvocationTargetException.class, () -> ReflectiveAccess.invoke(stop, proxy, new Object[]{true}));
assertThrows(java.lang.reflect.InvocationTargetException.class, () -> stop.invoke(proxy, true));
assertSame(manager, hosted.get(proxy));
}
@@ -716,14 +709,14 @@ class VotingPluginProxyLifecycleTest {
HostedControlManager manager = mock(HostedControlManager.class);
Field control = VotingPluginProxy.class.getDeclaredField("controlConnector");
control.setAccessible(true);
ReflectiveAccess.set(control, proxy, connector);
control.set(proxy, connector);
Field hosted = VotingPluginProxy.class.getDeclaredField("hostedControlManager");
hosted.setAccessible(true);
ReflectiveAccess.set(hosted, proxy, manager);
hosted.set(proxy, manager);
Method stop = VotingPluginProxy.class.getDeclaredMethod("stopControlServices", boolean.class);
stop.setAccessible(true);
ReflectiveAccess.invoke(stop, proxy, new Object[]{false});
stop.invoke(proxy, false);
verify(manager).close();
assertNull(control.get(proxy));
@@ -739,7 +732,7 @@ class VotingPluginProxyLifecycleTest {
clients.put("failing", failing);
clients.put("healthy", healthy);
VotingPluginProxyAccess.stopSocketClients(clients);
VotingPluginProxy.stopSocketClients(clients);
verify(failing).stopConnection();
verify(healthy).stopConnection();
@@ -764,18 +757,18 @@ class VotingPluginProxyLifecycleTest {
handles.setAccessible(true);
HashMap<String, ClientHandler> clients = new HashMap<>();
clients.put("lobby", client);
ReflectiveAccess.set(handles, proxy, clients);
handles.set(proxy, clients);
Method close = VotingPluginProxy.class.getDeclaredMethod("closeSocketClients");
close.setAccessible(true);
var executor = Executors.newFixedThreadPool(2);
try {
var send = executor.submit(() -> VotingPluginProxyAccess.sendProxyBroadcastEnvelopeNow(proxy, "lobby", envelope));
var send = executor.submit(() -> proxy.sendProxyBroadcastEnvelopeNow("lobby", envelope));
org.junit.jupiter.api.Assertions.assertTrue(sending.await(5, TimeUnit.SECONDS));
var closing = executor.submit(() -> {
closingStarted.countDown();
try {
ReflectiveAccess.invoke(close, proxy, new Object[]{});
close.invoke(proxy);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
@@ -1,16 +1,4 @@
package com.bencodez.votingplugin.tests.proxy;
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
import com.bencodez.votingplugin.proxy.BungeeMethod;
import com.bencodez.votingplugin.proxy.OfflineBungeeVote;
import com.bencodez.votingplugin.proxy.PendingIncomingVote;
import com.bencodez.votingplugin.proxy.PendingIncomingVoteJournal;
import com.bencodez.votingplugin.tests.proxy.PluginMessageDispatcherAccess;
import com.bencodez.votingplugin.tests.proxy.ReliableVoteDeliveryOutboxAccess;
import com.bencodez.votingplugin.proxy.VoteTotalsSnapshot;
import com.bencodez.votingplugin.proxy.VotingPluginProxy;
import com.bencodez.votingplugin.proxy.VotingPluginWire;
package com.bencodez.votingplugin.proxy;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
@@ -230,7 +218,7 @@ class VotingPluginProxyPacingTest {
proxy.handoffs.stream().map(handoff -> handoff.envelope().getSubChannel()).toList());
assertEquals(1, proxy.maxTransportDepth);
assertEquals(0, proxy.outbox.size());
assertEquals(0, new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat")).size());
assertEquals(0, new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat")).size());
assertPaced(proxy.handoffs);
}
@@ -238,17 +226,17 @@ class VotingPluginProxyPacingTest {
void fullReliableWindowAllowsReceiptsAndReleasesCapacityAfterDurableCompletion(@TempDir Path directory) throws Exception {
CapturingProxy proxy = new CapturingProxy(directory);
proxy.reliable().add(BACKEND);
for (int i = 0; i <= PluginMessageDispatcherAccess.MAX_IN_FLIGHT; i++)
for (int i = 0; i <= PluginMessageDispatcher.MAX_IN_FLIGHT; i++)
assertTrue(proxy.sendVoteEnvelopeAcceptedForTest(BACKEND, 1, vote(100 + i)));
proxy.time.advance(TimeUnit.SECONDS.toNanos(8));
assertEquals(PluginMessageDispatcherAccess.MAX_IN_FLIGHT, rewardHandoffs(proxy).size());
assertEquals(PluginMessageDispatcherAccess.MAX_IN_FLIGHT + 1, proxy.outbox.size());
assertEquals(PluginMessageDispatcher.MAX_IN_FLIGHT, rewardHandoffs(proxy).size());
assertEquals(PluginMessageDispatcher.MAX_IN_FLIGHT + 1, proxy.outbox.size());
UUID completed = new UUID(2, 100);
invoke(proxy, "handleVoteDeliveryAcknowledgement", JsonEnvelope.class,
VotingPluginWire.voteDeliveryAcknowledgement(BACKEND, completed, VotingPluginWire.SUB_VOTE));
proxy.time.advance(TimeUnit.SECONDS.toNanos(1));
assertEquals(PluginMessageDispatcherAccess.MAX_IN_FLIGHT + 1, rewardHandoffs(proxy).size());
assertEquals(PluginMessageDispatcher.MAX_IN_FLIGHT + 1, rewardHandoffs(proxy).size());
assertTrue(proxy.handoffs.stream().anyMatch(handoff -> VotingPluginWire.SUB_VOTE_DELIVERY_RECEIPT_RELEASE
.equals(handoff.envelope().getSubChannel())));
assertTrue(proxy.outbox.find(BACKEND, completed, VotingPluginWire.SUB_VOTE).orElseThrow().awaitingReceiptRelease());
@@ -259,7 +247,7 @@ class VotingPluginProxyPacingTest {
void outstandingReceiptsDoNotConsumeTheRewardWindow(@TempDir Path directory) throws Exception {
CapturingProxy proxy = new CapturingProxy(directory);
proxy.reliable().add(BACKEND);
for (int i = 0; i < PluginMessageDispatcherAccess.MAX_IN_FLIGHT + 1; i++) {
for (int i = 0; i < PluginMessageDispatcher.MAX_IN_FLIGHT + 1; i++) {
assertTrue(proxy.outbox.offer(BACKEND, vote(200 + i)));
assertTrue(proxy.outbox.acknowledgeCompletion(BACKEND, new UUID(2, 200 + i), VotingPluginWire.SUB_VOTE));
}
@@ -268,7 +256,7 @@ class VotingPluginProxyPacingTest {
proxy.time.advance(TimeUnit.SECONDS.toNanos(8));
assertTrue(rewardHandoffs(proxy).stream().anyMatch(handoff -> new UUID(2, 300).toString()
.equals(handoff.envelope().getFields().get(VotingPluginWire.K_VOTE_ID))));
assertEquals(PluginMessageDispatcherAccess.MAX_IN_FLIGHT + 1, proxy.handoffs.stream()
assertEquals(PluginMessageDispatcher.MAX_IN_FLIGHT + 1, proxy.handoffs.stream()
.filter(handoff -> VotingPluginWire.SUB_VOTE_DELIVERY_RECEIPT_RELEASE
.equals(handoff.envelope().getSubChannel())).count());
assertPaced(proxy.handoffs);
@@ -278,19 +266,19 @@ class VotingPluginProxyPacingTest {
void durableLegacyRetirementReleasesTheFullWindowBeforeReliableDeliveryReturns(@TempDir Path directory) throws Exception {
CapturingProxy proxy = new CapturingProxy(directory);
proxy.reliable().add(BACKEND);
for (int i = 0; i < PluginMessageDispatcherAccess.MAX_IN_FLIGHT; i++)
for (int i = 0; i < PluginMessageDispatcher.MAX_IN_FLIGHT; i++)
assertTrue(proxy.sendVoteEnvelopeAcceptedForTest(BACKEND, 1, vote(600 + i)));
proxy.time.advance(TimeUnit.SECONDS.toNanos(8));
assertEquals(PluginMessageDispatcherAccess.MAX_IN_FLIGHT, rewardHandoffs(proxy).size());
assertEquals(PluginMessageDispatcherAccess.MAX_IN_FLIGHT, proxy.outbox.size());
assertEquals(PluginMessageDispatcher.MAX_IN_FLIGHT, rewardHandoffs(proxy).size());
assertEquals(PluginMessageDispatcher.MAX_IN_FLIGHT, proxy.outbox.size());
proxy.reliable().remove(BACKEND);
proxy.legacy().add(BACKEND);
proxy.retry();
proxy.time.advance(TimeUnit.SECONDS.toNanos(1));
assertEquals(0, proxy.outbox.size());
assertEquals(0, new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat")).size());
assertEquals(2 * PluginMessageDispatcherAccess.MAX_IN_FLIGHT, rewardHandoffs(proxy).size());
assertEquals(0, new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat")).size());
assertEquals(2 * PluginMessageDispatcher.MAX_IN_FLIGHT, rewardHandoffs(proxy).size());
proxy.legacy().remove(BACKEND);
proxy.reliable().add(BACKEND);
@@ -318,7 +306,7 @@ class VotingPluginProxyPacingTest {
proxy.legacy().add(BACKEND);
proxy.retry();
assertEquals(0, proxy.outbox.size());
assertEquals(0, new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat")).size());
assertEquals(0, new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat")).size());
proxy.legacy().remove(BACKEND);
proxy.reliable().add(BACKEND);
@@ -578,7 +566,7 @@ class VotingPluginProxyPacingTest {
@Test
void saturatedControlLaneDoesNotAbortProbesForAnotherBackend(@TempDir Path directory) throws Exception {
CapturingProxy proxy = new CapturingProxy(directory);
for (int i = 0; i < PluginMessageDispatcherAccess.MAX_LANE_CONTROLS; i++)
for (int i = 0; i < PluginMessageDispatcher.MAX_LANE_CONTROLS; i++)
proxy.sendPluginMessageServer(BACKEND, 0, control("occupied"));
assertDoesNotThrow(proxy::sendServerNameMessage);
@@ -647,7 +635,7 @@ class VotingPluginProxyPacingTest {
when(proxy.getConfig().getProxyBroadcastScopeMode()).thenReturn("ALL_SERVERS");
setField(proxy, "proxyBroadcastDecider", new com.bencodez.votingplugin.proxy.broadcast.ProxyBroadcastDecider(
proxy::getConfig, proxy::getAllAvailableServers, proxy::isServerValid, ignored -> false));
for (int i = 0; i < PluginMessageDispatcherAccess.MAX_LANE_CONTROLS; i++)
for (int i = 0; i < PluginMessageDispatcher.MAX_LANE_CONTROLS; i++)
proxy.sendPluginMessageServer("backend-b", 0, control("occupied"));
UUID id = new UUID(2, 503);
String player = new UUID(1, 503).toString();
@@ -722,7 +710,7 @@ class VotingPluginProxyPacingTest {
restarted.cache.load();
assertTrue(restarted.retainIncomingVoteForRestart(emergency.load().get(0)));
com.bencodez.votingplugin.timequeue.VoteTimeQueue recovered = restarted.cache.getTimeChangeQueue().peek();
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(restarted, recovered,
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, restarted.replayQueuedVote(recovered,
VoteTotalsSnapshot.parseStorage(recovered.getTotals()), true));
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
assertTrue(restarted.handoffs.stream().noneMatch(h -> VotingPluginWire.SUB_VOTE_ONLINE
@@ -749,7 +737,7 @@ class VotingPluginProxyPacingTest {
original.setTotals(totals.toString());
original.setTotalsApplied(true);
assertTrue(proxy.cache.addTimeVoteToCache(original));
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.RETRY, VotingPluginProxyAccess.replayQueuedVote(proxy, original, totals, true));
assertEquals(VotingPluginProxy.QueuedVoteResult.RETRY, proxy.replayQueuedVote(original, totals, true));
proxy.closeDispatcher();
CapturingProxy restarted = new CapturingProxy(directory);
@@ -758,7 +746,7 @@ class VotingPluginProxyPacingTest {
restarted.playerServer = "backend-b";
com.bencodez.votingplugin.timequeue.VoteTimeQueue recovered = restarted.cache.getTimeChangeQueue().peek();
assertNotNull(recovered);
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(restarted, recovered,
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, restarted.replayQueuedVote(recovered,
VoteTotalsSnapshot.parseStorage(recovered.getTotals()), true));
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
assertTrue(restarted.handoffs.stream().noneMatch(h -> VotingPluginWire.SUB_VOTE_ONLINE
@@ -798,7 +786,7 @@ class VotingPluginProxyPacingTest {
enableLiveBroadcasts(restarted);
restarted.cache.load();
com.bencodez.votingplugin.timequeue.VoteTimeQueue recovered = restarted.cache.getTimeChangeQueue().peek();
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(restarted, recovered,
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, restarted.replayQueuedVote(recovered,
VoteTotalsSnapshot.parseStorage(recovered.getTotals()), true));
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
restarted.retryPendingOnlineBroadcasts();
@@ -832,7 +820,7 @@ class VotingPluginProxyPacingTest {
if (finishBeforeReplay) restarted.checkOnlineVotes("Synthetic", player, BACKEND);
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
com.bencodez.votingplugin.timequeue.VoteTimeQueue recovered = restarted.cache.getTimeChangeQueue().peek();
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(restarted, recovered,
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, restarted.replayQueuedVote(recovered,
VoteTotalsSnapshot.parseStorage(recovered.getTotals()), true));
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
restarted.retryPendingOnlineBroadcasts();
@@ -945,7 +933,7 @@ class VotingPluginProxyPacingTest {
restarted.playerServer = "backend-b";
restarted.cache.load();
com.bencodez.votingplugin.timequeue.VoteTimeQueue recovered = restarted.cache.getTimeChangeQueue().peek();
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(restarted, recovered,
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, restarted.replayQueuedVote(recovered,
VoteTotalsSnapshot.parseStorage(recovered.getTotals()), true));
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
restarted.checkOnlineVotes("Synthetic", player, "backend-b");
@@ -1002,7 +990,7 @@ class VotingPluginProxyPacingTest {
restarted.checkCachedVotes("backend-b");
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
com.bencodez.votingplugin.timequeue.VoteTimeQueue recovered = restarted.cache.getTimeChangeQueue().peek();
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(restarted, recovered,
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, restarted.replayQueuedVote(recovered,
VoteTotalsSnapshot.parseStorage(recovered.getTotals()), true));
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
restarted.checkCachedVotes("backend-b");
@@ -1043,7 +1031,7 @@ class VotingPluginProxyPacingTest {
restarted.cache.load();
assertTrue(restarted.retainIncomingVoteForRestart(rescue.load().get(0)));
com.bencodez.votingplugin.timequeue.VoteTimeQueue recovered = restarted.cache.getTimeChangeQueue().peek();
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(restarted, recovered,
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, restarted.replayQueuedVote(recovered,
VoteTotalsSnapshot.parseStorage(recovered.getTotals()), true));
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
List<String> targets = restarted.handoffs.stream().filter(h -> VotingPluginWire.SUB_VOTE
@@ -1070,7 +1058,7 @@ class VotingPluginProxyPacingTest {
}
assertTrue(proxy.cache.addTimeVoteToCache(source));
proxy.failAfterOnlineInsert = true;
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.RETRY, VotingPluginProxyAccess.replayQueuedVote(proxy, source, null, true));
assertEquals(VotingPluginProxy.QueuedVoteResult.RETRY, proxy.replayQueuedVote(source, null, true));
proxy.closeDispatcher();
CapturingProxy restarted = new CapturingProxy(directory);
@@ -1106,7 +1094,7 @@ class VotingPluginProxyPacingTest {
}
assertTrue(proxy.cache.addTimeVoteToCache(source));
assertTrue(proxy.cache.addOnlineVoteDurably(player, owner));
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(proxy, source, null, true));
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, proxy.replayQueuedVote(source, null, true));
assertTrue(owner.isRewardDelivered(), "replay left a stale incomplete canonical owner");
proxy.cache.getTimeChangeQueue().clear();
proxy.retryPendingOnlineBroadcasts();
@@ -1222,7 +1210,7 @@ class VotingPluginProxyPacingTest {
CapturingProxy proxy = new CapturingProxy(directory);
proxy.legacy().addAll(List.of(BACKEND, "backend-b"));
when(proxy.getConfig().getOnlineMode()).thenReturn(true);
for (int i = 0; i < PluginMessageDispatcherAccess.MAX_LANE_CONTROLS; i++)
for (int i = 0; i < PluginMessageDispatcher.MAX_LANE_CONTROLS; i++)
proxy.sendPluginMessageServer("backend-b", 0, control("occupied"));
UUID id = new UUID(2, 500);
String player = new UUID(1, 500).toString();
@@ -1247,7 +1235,7 @@ class VotingPluginProxyPacingTest {
assertDoesNotThrow(() -> proxy.vote("Synthetic", "synthetic-service", true, false, 1L, null, player, id));
proxy.time.advance(TimeUnit.SECONDS.toNanos(1));
assertEquals(List.of(BACKEND), updateHandoffs(proxy).stream().map(Handoff::server).toList());
assertEquals(0, (PluginMessageDispatcherAccess.view(field(proxy, "pluginMessageDispatcher"))).pendingControls());
assertEquals(0, ((PluginMessageDispatcher) field(proxy, "pluginMessageDispatcher")).pendingControls());
assertEquals(1, proxy.outbox.size());
proxy.closeDispatcher();
@@ -1256,7 +1244,7 @@ class VotingPluginProxyPacingTest {
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
assertEquals(List.of("backend-b"), updateHandoffs(restarted).stream().map(Handoff::server).toList());
assertEquals(id.toString(), updateHandoffs(restarted).get(0).envelope().getFields().get(VotingPluginWire.K_VOTE_ID));
assertEquals(0, new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat")).size());
assertEquals(0, new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat")).size());
invoke(restarted, "retryReliableVoteDeliveries", String.class, null);
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
assertEquals(1, updateHandoffs(restarted).size());
@@ -1309,7 +1297,7 @@ class VotingPluginProxyPacingTest {
assertEquals(List.of(prepared, unprepared), online ? proxy.cache.getOnlineVotes(owner) : proxy.cache.getVotes(owner),
"failed completion persistence removed a cache owner");
String subChannel = online ? VotingPluginWire.SUB_VOTE_ONLINE : VotingPluginWire.SUB_VOTE;
ReliableVoteDeliveryOutboxAccess durable = new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat"));
ReliableVoteDeliveryOutbox durable = new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat"));
if (reliable && !dirty) {
assertEquals(1, durable.size(), "unprepared sibling blocked a clean reliable batch");
assertSingleVoteBatch(durable.snapshot().get(0).envelope(), subChannel, prepared.getVoteId());
@@ -1435,7 +1423,7 @@ class VotingPluginProxyPacingTest {
restarted.playerServer = "backend-b";
com.bencodez.votingplugin.timequeue.VoteTimeQueue recovered = restarted.cache.getTimeChangeQueue().peek();
assertNotNull(recovered);
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(restarted, recovered, totals, true));
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, restarted.replayQueuedVote(recovered, totals, true));
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
assertTrue(restarted.handoffs.stream().noneMatch(h -> VotingPluginWire.SUB_VOTE.equals(h.envelope().getSubChannel())
|| VotingPluginWire.SUB_VOTE_ONLINE.equals(h.envelope().getSubChannel())),
@@ -1487,7 +1475,7 @@ class VotingPluginProxyPacingTest {
assertFalse(restarted.outbox.find(BACKEND, id, VotingPluginWire.SUB_VOTE_ONLINE).isPresent());
com.bencodez.votingplugin.timequeue.VoteTimeQueue recovered = restarted.cache.getTimeChangeQueue().peek();
assertNotNull(recovered);
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(restarted, recovered, totals, true));
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, restarted.replayQueuedVote(recovered, totals, true));
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
assertTrue(restarted.handoffs.stream().noneMatch(h -> VotingPluginWire.SUB_VOTE.equals(h.envelope().getSubChannel())
|| VotingPluginWire.SUB_VOTE_ONLINE.equals(h.envelope().getSubChannel())),
@@ -1521,7 +1509,7 @@ class VotingPluginProxyPacingTest {
restarted.cache.load();
com.bencodez.votingplugin.timequeue.VoteTimeQueue recovered = restarted.cache.getTimeChangeQueue().peek();
assertNotNull(recovered);
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(restarted, recovered,
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, restarted.replayQueuedVote(recovered,
VoteTotalsSnapshot.parseStorage(recovered.getTotals()), true));
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
assertTrue(updateHandoffs(restarted).isEmpty(), "restart replayed updates that already handed off");
@@ -1551,7 +1539,7 @@ class VotingPluginProxyPacingTest {
restarted.cache.load();
com.bencodez.votingplugin.timequeue.VoteTimeQueue recovered = restarted.cache.getTimeChangeQueue().peek();
assertNotNull(recovered);
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(restarted, recovered, totals, true));
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, restarted.replayQueuedVote(recovered, totals, true));
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
assertTrue(updateHandoffs(restarted).stream().noneMatch(h -> BACKEND.equals(h.server())),
"resolving a timed source repeated its already fenced update");
@@ -1574,7 +1562,7 @@ class VotingPluginProxyPacingTest {
assertTrue(proxy.sendPluginMessageImmediately(BACKEND,
JsonEnvelope.builder("SyntheticControl").put("value", "occupied").build()));
proxy.failAfterOnlineInsert = true;
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.RETRY, VotingPluginProxyAccess.replayQueuedVote(proxy, source, null, true));
assertEquals(VotingPluginProxy.QueuedVoteResult.RETRY, proxy.replayQueuedVote(source, null, true));
assertEquals(1, proxy.cache.getOnlineVotes(player).size());
assertFalse(proxy.cache.getOnlineVotes(player).get(0).isRewardDelivered());
assertTrue(proxy.retainIncomingVoteForRestart(
@@ -1660,7 +1648,7 @@ class VotingPluginProxyPacingTest {
restarted.cache.load();
com.bencodez.votingplugin.timequeue.VoteTimeQueue recovered = restarted.cache.getTimeChangeQueue().peek();
assertNotNull(recovered);
assertEquals(VotingPluginProxyAccess.QueuedVoteResult.SUCCESS, VotingPluginProxyAccess.replayQueuedVote(restarted, recovered,
assertEquals(VotingPluginProxy.QueuedVoteResult.SUCCESS, restarted.replayQueuedVote(recovered,
VoteTotalsSnapshot.parseStorage(recovered.getTotals()), true));
restarted.time.advance(TimeUnit.SECONDS.toNanos(1));
assertTrue(restarted.handoffs.stream().noneMatch(h -> VotingPluginWire.SUB_VOTE.equals(h.envelope().getSubChannel())
@@ -1684,7 +1672,7 @@ class VotingPluginProxyPacingTest {
setField(proxy, "voteLogMysqlTable", log);
String player = new UUID(11, 1).toString();
Set<String> existingUpdateIds = new LinkedHashSet<>();
for (int i = 0; i < ReliableVoteDeliveryOutboxAccess.MAX_UPDATE_ENTRIES; i++) {
for (int i = 0; i < ReliableVoteDeliveryOutbox.MAX_UPDATE_ENTRIES; i++) {
UUID id = new UUID(12, i);
existingUpdateIds.add(id.toString());
assertTrue(proxy.outbox.offer("backend-b", VotingPluginWire.voteUpdate(player, 1, 10,
@@ -1713,7 +1701,7 @@ class VotingPluginProxyPacingTest {
.map(handoff -> handoff.envelope().getFields().get(VotingPluginWire.K_VOTE_ID))
.collect(java.util.stream.Collectors.toSet()), "an empty backend blocked another backend's reward");
assertTrue(updateHandoffs(proxy).stream().noneMatch(handoff -> "backend-b".equals(handoff.server())));
assertTrue(new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat")).snapshot().stream()
assertTrue(new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat")).snapshot().stream()
.map(entry -> entry.envelope().getFields().get(VotingPluginWire.K_VOTE_ID))
.collect(java.util.stream.Collectors.toSet()).containsAll(existingUpdateIds),
"quota handling discarded existing update occurrences");
@@ -1739,7 +1727,7 @@ class VotingPluginProxyPacingTest {
"deferred updates lost their original vote IDs across restart");
assertEquals(expectedUpdateIds.size(), deliveredUpdateIds.size(), "restart repeated an accepted update");
assertTrue(restarted.cache.getTimeChangeQueue().isEmpty(), "settled source rows remained queued");
assertEquals(0, new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat")).size());
assertEquals(0, new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat")).size());
assertTrue(restarted.handoffs.stream().noneMatch(handoff -> VotingPluginWire.SUB_VOTE_ONLINE
.equals(handoff.envelope().getSubChannel())), "restart repeated an accepted reward");
for (UUID id : newVoteIds) {
@@ -1820,7 +1808,7 @@ class VotingPluginProxyPacingTest {
final List<Handoff> handoffs = new ArrayList<>();
final VoteCacheHandler cache;
final BungeeJsonVoteCache storage;
final ReliableVoteDeliveryOutboxAccess outbox;
final ReliableVoteDeliveryOutbox outbox;
boolean carrier = true;
boolean broadcastValidationEnabled;
final Set<String> unavailableBackends = new LinkedHashSet<>();
@@ -1880,7 +1868,7 @@ class VotingPluginProxyPacingTest {
@Override public void debug1(Throwable failure) { }
@Override public void debug1(String message) { }
};
outbox = new ReliableVoteDeliveryOutboxAccess(directory.resolve("outbox.dat"));
outbox = new ReliableVoteDeliveryOutbox(directory.resolve("outbox.dat"));
setField(this, "voteCacheHandler", cache);
setField(this, "reliableVoteDeliveryOutbox", outbox);
GlobalMessageProxyHandler messages = mock(GlobalMessageProxyHandler.class);
@@ -1966,7 +1954,7 @@ class VotingPluginProxyPacingTest {
return (Set<String>) field(this, "reliableVoteDeliveryServers");
}
void retry() { invoke(this, "retryReliableVoteDeliveries", String.class, BACKEND); }
void closeDispatcher() throws Exception { (PluginMessageDispatcherAccess.view(field(this, "pluginMessageDispatcher"))).close(); }
void closeDispatcher() throws Exception { ((PluginMessageDispatcher) field(this, "pluginMessageDispatcher")).close(); }
}
static final class ManualTime {
@@ -2020,14 +2008,14 @@ class VotingPluginProxyPacingTest {
static void setField(Object target, String name, Object value) throws ReflectiveOperationException {
Field field = VotingPluginProxy.class.getDeclaredField(name);
field.setAccessible(true);
ReflectiveAccess.set(field, target, value);
field.set(target, value);
}
private static void invoke(Object target, String name, Class<?> type, Object value) {
try {
Method method = VotingPluginProxy.class.getDeclaredMethod(name, type);
method.setAccessible(true);
ReflectiveAccess.invoke(method, target, new Object[]{value});
method.invoke(target, value);
} catch (InvocationTargetException failure) {
throw new AssertionError(failure.getCause());
} catch (ReflectiveOperationException failure) {
@@ -1,8 +1,4 @@
package com.bencodez.votingplugin.tests.proxy;
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
import com.bencodez.votingplugin.proxy.VotingPluginProxy;
package com.bencodez.votingplugin.proxy;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -33,8 +29,8 @@ class VotingPluginProxyQueueProgressTest {
Mockito.when(cache.removeTimeVote(second)).thenAnswer(invocation -> queue.remove(second));
VotingPluginProxy proxy = Mockito.spy(new VotingPluginProxyTestImpl());
Mockito.doReturn(cache).when(proxy).getVoteCacheHandler();
VotingPluginProxyAccess.replayQueuedVote(Mockito.doReturn(ReflectiveAccess.field("com.bencodez.votingplugin.proxy.VotingPluginProxy$QueuedVoteResult", "RETRY_NONBLOCKING"))
.when(proxy), Mockito.eq(first), Mockito.isNull(), Mockito.anyBoolean());
Mockito.doReturn(VotingPluginProxy.QueuedVoteResult.RETRY_NONBLOCKING)
.when(proxy).replayQueuedVote(Mockito.eq(first), Mockito.isNull(), Mockito.anyBoolean());
proxy.processQueue();
@@ -54,8 +50,8 @@ class VotingPluginProxyQueueProgressTest {
Mockito.when(cache.getTimeChangeQueue()).thenReturn(queue);
VotingPluginProxy proxy = Mockito.spy(new VotingPluginProxyTestImpl());
Mockito.doReturn(cache).when(proxy).getVoteCacheHandler();
VotingPluginProxyAccess.replayQueuedVote(Mockito.doReturn(ReflectiveAccess.field("com.bencodez.votingplugin.proxy.VotingPluginProxy$QueuedVoteResult", "RETRY"))
.when(proxy), Mockito.eq(first), Mockito.isNull(), Mockito.anyBoolean());
Mockito.doReturn(VotingPluginProxy.QueuedVoteResult.RETRY)
.when(proxy).replayQueuedVote(Mockito.eq(first), Mockito.isNull(), Mockito.anyBoolean());
proxy.processQueue();
@@ -1,6 +1,4 @@
package com.bencodez.votingplugin.tests.proxy.bungee;
import com.bencodez.votingplugin.proxy.bungee.VotingPluginBungee;
package com.bencodez.votingplugin.proxy.bungee;
import java.util.*;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@@ -10,7 +8,7 @@ class NetworkHealthInventoryTest {
net.md_5.bungee.api.plugin.Plugin plugin = mock(net.md_5.bungee.api.plugin.Plugin.class);
var description = mock(net.md_5.bungee.api.plugin.PluginDescription.class);
when(plugin.getDescription()).thenReturn(description); when(description.getName()).thenReturn("VotifierPlus");
assertEquals(List.of("VotifierPlus"), VotingPluginBungeeAccess.installedPluginNames(List.of(plugin)));
assertEquals(128, VotingPluginBungeeAccess.installedPluginNames(Collections.nCopies(129, plugin)).size());
assertEquals(List.of("VotifierPlus"), VotingPluginBungee.installedPluginNames(List.of(plugin)));
assertEquals(128, VotingPluginBungee.installedPluginNames(Collections.nCopies(129, plugin)).size());
}
}
@@ -1,10 +1,4 @@
package com.bencodez.votingplugin.tests.proxy.bungee;
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
import com.bencodez.votingplugin.proxy.bungee.BungeeConfig;
import com.bencodez.votingplugin.proxy.bungee.VoteEventBungee;
import com.bencodez.votingplugin.proxy.bungee.VotingPluginBungee;
package com.bencodez.votingplugin.proxy.bungee;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -49,7 +43,7 @@ class VotingPluginBungeeInitializationTest {
return null;
}).when(plugin).reloadPlugin(true);
VotingPluginBungeeAccess.initializeFirstRuntime(plugin);
plugin.initializeFirstRuntime();
assertEquals(1, reloadCalls.get());
}
@@ -59,7 +53,7 @@ class VotingPluginBungeeInitializationTest {
VotingPluginBungee plugin = mock(VotingPluginBungee.class, CALLS_REAL_METHODS);
doAnswer(invocation -> null).when(plugin).reloadPlugin(true);
VotingPluginBungeeAccess.initializeFirstRuntime(plugin);
plugin.initializeFirstRuntime();
assertFalse(plugin.isRuntimeOperational());
}
@@ -82,7 +76,7 @@ class VotingPluginBungeeInitializationTest {
doThrow(new RuntimeException("registration failed")).when(manager)
.registerListener(eq(plugin), any(VoteEventBungee.class));
assertFalse(VotingPluginBungeeAccess.initVotifierListenerIfNeeded(plugin));
assertFalse(plugin.initVotifierListenerIfNeeded());
assertFalse(plugin.isRuntimeOperational());
}
@@ -91,11 +85,11 @@ class VotingPluginBungeeInitializationTest {
VotingPluginBungee plugin = mock(VotingPluginBungee.class, CALLS_REAL_METHODS);
VotingPluginProxy runtime = mock(VotingPluginProxy.class);
when(plugin.getVotingPluginProxy()).thenReturn(runtime);
VotingPluginBungeeAccess.requireVotifierEventClass(doThrow(new ClassNotFoundException("absent")).when(plugin));
doThrow(new ClassNotFoundException("absent")).when(plugin).requireVotifierEventClass();
assertTrue(VotingPluginBungeeAccess.initVotifierListenerIfNeeded(plugin));
assertTrue(plugin.initVotifierListenerIfNeeded());
verify(runtime).setVotifierEnabled(false);
VotingPluginBungeeAccess.createVotifierListener(verify(plugin, never()));
verify(plugin, never()).createVotifierListener();
}
@Test
@@ -105,8 +99,8 @@ class VotingPluginBungeeInitializationTest {
when(plugin.getVotingPluginProxy()).thenReturn(runtime);
when(runtime.isVotifierEnabled()).thenReturn(false);
assertTrue(VotingPluginBungeeAccess.initVotifierListenerIfNeeded(plugin));
VotingPluginBungeeAccess.createVotifierListener(verify(plugin, never()));
assertTrue(plugin.initVotifierListenerIfNeeded());
verify(plugin, never()).createVotifierListener();
}
@Test
@@ -121,9 +115,9 @@ class VotingPluginBungeeInitializationTest {
when(runtime.isVotifierEnabled()).thenReturn(true);
when(plugin.getProxy()).thenReturn(proxy);
when(proxy.getPluginManager()).thenReturn(manager);
when(VotingPluginBungeeAccess.createVotifierListener(plugin)).thenReturn(listener);
when(plugin.createVotifierListener()).thenReturn(listener);
assertTrue(VotingPluginBungeeAccess.initVotifierListenerIfNeeded(plugin));
assertTrue(plugin.initVotifierListenerIfNeeded());
verify(manager).registerListener(plugin, listener);
assertSame(listener, getField(plugin, "voteEventBungee"));
}
@@ -179,7 +173,7 @@ class VotingPluginBungeeInitializationTest {
java.lang.reflect.Method persist = VotingPluginBungee.class.getDeclaredMethod(
"persistPendingIncomingVotes", VotingPluginProxy.class, String.class);
persist.setAccessible(true);
assertTrue((Boolean) ReflectiveAccess.invoke(persist, plugin, new Object[]{runtime, "test shutdown"}));
assertTrue((Boolean) persist.invoke(plugin, runtime, "test shutdown"));
org.mockito.ArgumentCaptor<PendingIncomingVote> retained =
org.mockito.ArgumentCaptor.forClass(PendingIncomingVote.class);
verify(runtime).retainIncomingVoteForRestart(retained.capture());
@@ -213,7 +207,7 @@ class VotingPluginBungeeInitializationTest {
"persistTerminalPendingVote", PendingIncomingVote.class);
persist.setAccessible(true);
ReflectiveAccess.invoke(persist, plugin, new Object[]{pending});
persist.invoke(plugin, pending);
assertEquals(1, queue.size());
assertTrue(retry.get() != null);
@@ -255,7 +249,7 @@ class VotingPluginBungeeInitializationTest {
java.lang.reflect.Method persist = VotingPluginBungee.class.getDeclaredMethod(
"persistPendingIncomingVotes", VotingPluginProxy.class, String.class);
persist.setAccessible(true);
assertTrue((Boolean) ReflectiveAccess.invoke(persist, plugin, new Object[]{runtime, "test shutdown"}));
assertTrue((Boolean) persist.invoke(plugin, runtime, "test shutdown"));
org.mockito.ArgumentCaptor<PendingIncomingVote> retained =
org.mockito.ArgumentCaptor.forClass(PendingIncomingVote.class);
verify(runtime).retainIncomingVoteForRestart(retained.capture());
@@ -289,7 +283,7 @@ class VotingPluginBungeeInitializationTest {
"persistPendingIncomingVotes", VotingPluginProxy.class, String.class);
persist.setAccessible(true);
assertTrue((Boolean) ReflectiveAccess.invoke(persist, plugin, new Object[]{runtime, "test shutdown"}));
assertTrue((Boolean) persist.invoke(plugin, runtime, "test shutdown"));
assertEquals(0, ((PendingIncomingVoteQueue) getField(plugin, "pendingIncomingVotes")).size());
assertEquals(1, new PendingIncomingVoteJournal(dataDirectory).load().size());
}
@@ -314,7 +308,7 @@ class VotingPluginBungeeInitializationTest {
"persistPendingIncomingVotes", VotingPluginProxy.class, String.class);
persist.setAccessible(true);
assertTrue((Boolean) ReflectiveAccess.invoke(persist, plugin, new Object[]{runtime, "test shutdown"}));
assertTrue((Boolean) persist.invoke(plugin, runtime, "test shutdown"));
assertEquals(0, queue.size());
verify(rescue).merge(any());
@@ -398,7 +392,7 @@ class VotingPluginBungeeInitializationTest {
.getDeclaredMethod("retireFailedReplacementRuntime");
retire.setAccessible(true);
ReflectiveAccess.invoke(retire, plugin, new Object[]{});
retire.invoke(plugin);
verify(runtime).onDisable();
assertFalse(plugin.isRuntimeOperational());
@@ -410,7 +404,7 @@ class VotingPluginBungeeInitializationTest {
setField(plugin, "runtimeInitialized", true);
setField(plugin, "runtimeOperational", false);
VotingPluginBungeeAccess.publishRetainedRuntimeOperational(plugin);
plugin.publishRetainedRuntimeOperational();
assertTrue(plugin.isRuntimeOperational());
}
@@ -422,7 +416,7 @@ class VotingPluginBungeeInitializationTest {
setField(plugin, "runtimeInitialized", false);
setField(plugin, "runtimeOperational", true);
VotingPluginBungeeAccess.publishRetainedRuntimeOperational(plugin);
plugin.publishRetainedRuntimeOperational();
assertFalse(plugin.isRuntimeOperational());
}
@@ -430,7 +424,7 @@ class VotingPluginBungeeInitializationTest {
private static void setField(Object target, String name, Object value) throws Exception {
java.lang.reflect.Field field = VotingPluginBungee.class.getDeclaredField(name);
field.setAccessible(true);
ReflectiveAccess.set(field, target, value);
field.set(target, value);
}
private static Object getField(Object target, String name) throws Exception {
@@ -1,13 +1,4 @@
package com.bencodez.votingplugin.tests.proxy.cache;
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
import com.bencodez.votingplugin.proxy.cache.IVoteCache;
import com.bencodez.votingplugin.proxy.cache.PendingPluginMessageVotePartyRewards;
import com.bencodez.votingplugin.proxy.cache.PendingVotePartyProxyEffects;
import com.bencodez.votingplugin.proxy.cache.VoteCacheDurability;
import com.bencodez.votingplugin.proxy.cache.VoteCacheHandler;
import com.bencodez.votingplugin.proxy.cache.VotePartyCacheDurability;
package com.bencodez.votingplugin.proxy.cache;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@@ -230,7 +221,7 @@ class PendingPluginMessageVotePartyRewardsTest {
assertThrows(IllegalStateException.class,
() -> handler(null).runVotePartyCacheTransaction(() -> fail("unavailable callback ran")));
java.lang.reflect.Field quarantine = VoteCacheHandler.class.getDeclaredField("jsonStorageQuarantined");
quarantine.setAccessible(true); ReflectiveAccess.set(quarantine, handler, true);
quarantine.setAccessible(true); quarantine.set(handler, true);
assertThrows(IllegalStateException.class, handler::getPluginMessageVotePartyRewards);
assertThrows(IllegalStateException.class, () -> handler.setPluginMessageVotePartyRewards(snapshot()));
assertThrows(IllegalStateException.class,
@@ -1,8 +1,4 @@
package com.bencodez.votingplugin.tests.proxy.cache;
import com.bencodez.votingplugin.proxy.cache.IVoteCache;
import com.bencodez.votingplugin.proxy.cache.PendingVotePartyProxyEffects;
import com.bencodez.votingplugin.proxy.cache.VotePartyCacheDurability;
package com.bencodez.votingplugin.proxy.cache;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
@@ -1,8 +1,4 @@
package com.bencodez.votingplugin.tests.proxy.control;
import com.bencodez.votingplugin.tests.support.ReflectiveAccess;
import com.bencodez.votingplugin.proxy.control.ControlConnector;
package com.bencodez.votingplugin.proxy.control;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@@ -19,14 +15,14 @@ class ControlNetworkHealthTest {
private static final UUID SESSION = UUID.randomUUID();
private static final String ID = UUID.randomUUID().toString(), ATTEMPT = UUID.randomUUID().toString();
private static void field(ControlConnector connector, String name, Object value) throws Exception {
Field field = ControlConnector.class.getDeclaredField(name); field.setAccessible(true); ReflectiveAccess.set(field, connector, value);
Field field = ControlConnector.class.getDeclaredField(name); field.setAccessible(true); field.set(connector, value);
}
private static void poll(ControlConnector connector) throws Exception {
var method = ControlConnector.class.getDeclaredMethod("pollNetworkHealth"); method.setAccessible(true); method.invoke(connector);
}
private ControlConnector connector(String platform, ControlConnector.Transport transport, ScheduledExecutorService executor) throws Exception {
var settings = new ControlConnector.Settings("proxy-a", "a", platform, "1", URI.create("http://localhost:8080"), 10, 1000, 1000);
var connector = ControlConnectorAccess.create(settings, executor, transport, List::of, ignored -> {}, SESSION, () -> 0);
var connector = new ControlConnector(settings, executor, transport, List::of, ignored -> {}, SESSION, () -> 0);
var proxy = mock(VotingPluginProxy.class); var config = mock(VotingPluginProxyConfig.class);
when(proxy.getConfig()).thenReturn(config);
when(proxy.getInstalledPluginNames()).thenReturn(List.of("VotingPlugin", "VotifierPlus"));
Loaded 100 of 248 files, more files were not shown because too many files have changed in this diff. Show more