mirror of
https://github.com/BenCodez/VotingPlugin.git
synced 2026-11-15 05:32:06 -05:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f6cb104bf | ||
|
|
7010625601 | ||
|
|
8bb8851340 | ||
|
|
1d89eeca4d | ||
|
|
d2d9256cd3 | ||
|
|
41cca3f51f |
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ public class CommandAliases implements CommandExecutor {
|
||||
if (adminCommand) {
|
||||
sender.sendMessage(MessageAPI.colorize(plugin.getConfigFile().getFormatInvalidCommandAdminVote()));
|
||||
} else {
|
||||
sender.sendMessage(MessageAPI.colorize(plugin.formatVoteCommandMessage(plugin.getConfigFile().getFormatInvalidCommandVote())));
|
||||
sender.sendMessage(MessageAPI.colorize(plugin.getConfigFile().getFormatInvalidCommandVote()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+8
-8
@@ -77,16 +77,16 @@ public class AdminVotePerms extends GUIHandler {
|
||||
for (CommandHandler handle : plugin.getVoteCommand()) {
|
||||
if (sender instanceof Player) {
|
||||
if (handle.hasPerm(sender)) {
|
||||
msg.add("&6" + handle.getHelpLineCommand(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)) {
|
||||
|
||||
+3
-5
@@ -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<>();
|
||||
|
||||
+1
-1
@@ -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";
|
||||
|
||||
-127
@@ -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;
|
||||
}
|
||||
}
|
||||
-212
@@ -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;
|
||||
}
|
||||
}
|
||||
-240
@@ -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;
|
||||
}
|
||||
}
|
||||
-21
@@ -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"; };
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
-4
@@ -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;
|
||||
+12
-16
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
-6
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+119
-125
@@ -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();
|
||||
+37
-40
@@ -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));
|
||||
+11
-15
@@ -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);
|
||||
}
|
||||
+4
-6
@@ -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());
|
||||
+3
-7
@@ -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) {
|
||||
+88
-93
@@ -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 {
|
||||
+8
-12
@@ -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);
|
||||
}
|
||||
}
|
||||
+75
-81
@@ -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)
|
||||
+7
-11
@@ -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
-1
@@ -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
-3
@@ -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;
|
||||
+4
-7
@@ -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());
|
||||
+14
-16
@@ -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();
|
||||
+15
-22
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-8
@@ -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);
|
||||
+8
-10
@@ -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");
|
||||
}
|
||||
}
|
||||
+14
-16
@@ -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));
|
||||
}
|
||||
|
||||
+3
-5
@@ -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());
|
||||
}
|
||||
}
|
||||
+114
-116
@@ -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"));
|
||||
+3
-6
@@ -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 =
|
||||
+90
-96
@@ -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());
|
||||
}
|
||||
}
|
||||
+12
-15
@@ -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());
|
||||
}
|
||||
}
|
||||
+39
-41
@@ -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\"}}")
|
||||
+15
-17
@@ -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());
|
||||
}
|
||||
|
||||
+14
-17
@@ -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
-3
@@ -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;
|
||||
+28
-30
@@ -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
-1
@@ -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
-3
@@ -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
-3
@@ -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
-4
@@ -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
-3
@@ -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
-3
@@ -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
-3
@@ -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
-3
@@ -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
-1
@@ -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;
|
||||
+3
-5
@@ -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));
|
||||
}
|
||||
}
|
||||
+192
-204
@@ -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();
|
||||
}
|
||||
|
||||
+88
-98
@@ -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);
|
||||
+162
-179
@@ -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)));
|
||||
+12
-17
@@ -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; },
|
||||
+8
-12
@@ -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"));
|
||||
+7
-11
@@ -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());
|
||||
+22
-28
@@ -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"));
|
||||
}
|
||||
}
|
||||
+4
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+37
-41
@@ -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
-3
@@ -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;
|
||||
+6
-9
@@ -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
-4
@@ -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;
|
||||
+24
-26
@@ -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
-4
@@ -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;
|
||||
+3
-6
@@ -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());
|
||||
}
|
||||
}
|
||||
+41
-44
@@ -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());
|
||||
}
|
||||
|
||||
+3
-9
@@ -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
-3
@@ -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;
|
||||
+65
-72
@@ -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);
|
||||
}
|
||||
+44
-56
@@ -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) {
|
||||
+5
-9
@@ -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();
|
||||
|
||||
+3
-5
@@ -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());
|
||||
}
|
||||
}
|
||||
+20
-26
@@ -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 {
|
||||
+2
-11
@@ -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
-5
@@ -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;
|
||||
+209
-204
File diff suppressed because it is too large.
Load diff
+3
-7
@@ -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
Reference in new issue
Block a user