From 4ee7af2c1f013476784eda5f91b763f6f47941c4 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 19 Jul 2026 13:38:52 -0600 Subject: [PATCH] Preserve vote IDs through proxy cache and time queue --- .../votingplugin/proxy/VotingPluginProxy.java | 18 +- .../proxy/bungee/BungeeJsonVoteCache.java | 5 +- .../proxy/cache/ProxyTimedVoteCacheTable.java | 59 +++++- .../proxy/cache/VoteCacheHandler.java | 100 ++++++++- .../proxy/velocity/VelocityJsonVoteCache.java | 6 +- .../votingplugin/timequeue/VoteTimeQueue.java | 28 +++ .../tests/VoteCacheHandlerVoteIdTest.java | 189 ++++++++++++++++++ 7 files changed, 386 insertions(+), 19 deletions(-) create mode 100644 VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VoteCacheHandlerVoteIdTest.java diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java index e9daca778..22e5baf25 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginProxy.java @@ -1342,7 +1342,7 @@ private UUID parseUUIDFromString(String uuidAsString) { public void processQueue() { while (getVoteCacheHandler().getTimeChangeQueue().size() > 0) { VoteTimeQueue vote = getVoteCacheHandler().getTimeChangeQueue().remove(); - vote(vote.getName(), vote.getService(), true, false, vote.getTime(), null, null); + vote(vote.getName(), vote.getService(), true, false, vote.getTime(), null, null, vote.getVoteId()); } } @@ -1597,7 +1597,19 @@ public boolean checkVoteDelay(String uuid, String service, ArrayList dat public synchronized void vote(String player, String service, boolean realVote, boolean timeQueue, long queueTime, VoteTotalsSnapshot text, String uuid) { + vote(player, service, realVote, timeQueue, queueTime, text, uuid, null); + } + + private synchronized void vote(String player, String service, boolean realVote, boolean timeQueue, long queueTime, + VoteTotalsSnapshot text, String uuid, UUID existingVoteId) { try { + UUID voteId = existingVoteId; + if (voteId == null && text != null) { + voteId = text.getVoteUUID(); + } + if (voteId == null) { + voteId = UUID.randomUUID(); + } if (player == null || player.isEmpty()) { log("No name from vote on " + service); return; @@ -1608,7 +1620,7 @@ public synchronized void vote(String player, String service, boolean realVote, b if (getGlobalDataHandler().isTimeChangedHappened()) { getGlobalDataHandler().checkForFinishedTimeChanges(); if (timeQueue && getGlobalDataHandler().isTimeChangedHappened()) { - getVoteCacheHandler().getTimeChangeQueue().add(new VoteTimeQueue(player, service, + getVoteCacheHandler().getTimeChangeQueue().add(new VoteTimeQueue(voteId, player, service, LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli())); log("Caching vote from " + player + "/" + service + " because time change is happening right now"); @@ -1684,8 +1696,6 @@ public synchronized void vote(String player, String service, boolean realVote, b final boolean playerOnline = isPlayerOnline(player); final String playerServer = playerOnline ? getCurrentPlayerServer(player) : null; - final UUID voteId = UUID.randomUUID(); - addVoteParty(); // Totals processing (primary server OR no multiproxy) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/bungee/BungeeJsonVoteCache.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/bungee/BungeeJsonVoteCache.java index 41875cbdd..a147ca22d 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/bungee/BungeeJsonVoteCache.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/bungee/BungeeJsonVoteCache.java @@ -40,6 +40,7 @@ public void addTimedVote(int num, VoteTimeQueue voteTimedQueue) { setString(path + ".Name", voteTimedQueue.getName()); setString(path + ".Service", voteTimedQueue.getService()); setLong(path + ".Time", voteTimedQueue.getTime()); + setString(path + ".VoteId", voteTimedQueue.getVoteId() == null ? null : voteTimedQueue.getVoteId().toString()); } public void addVote(String server, int num, OfflineBungeeVote voteData) { @@ -50,7 +51,7 @@ public void addVote(String server, int num, OfflineBungeeVote voteData) { setLong(path + ".Time", voteData.getTime()); setBoolean(path + ".Real", voteData.isRealVote()); setString(path + ".Text", voteData.getText()); - setString(path + ".VoteID", voteData.getVoteId() != null ? voteData.getVoteId().toString() : null); + setString(path + ".VoteId", voteData.getVoteId() != null ? voteData.getVoteId().toString() : null); } public void addVoteOnline(String player, int num, OfflineBungeeVote voteData) { @@ -61,7 +62,7 @@ public void addVoteOnline(String player, int num, OfflineBungeeVote voteData) { setLong(path + ".Time", voteData.getTime()); setBoolean(path + ".Real", voteData.isRealVote()); setString(path + ".Text", voteData.getText()); - setString(path + ".VoteID", voteData.getVoteId() != null ? voteData.getVoteId().toString() : null); + setString(path + ".VoteId", voteData.getVoteId() != null ? voteData.getVoteId().toString() : null); } public void clearData() { diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/ProxyTimedVoteCacheTable.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/ProxyTimedVoteCacheTable.java index 1eaf33ba8..8338b203c 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/ProxyTimedVoteCacheTable.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/ProxyTimedVoteCacheTable.java @@ -6,6 +6,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import com.bencodez.simpleapi.sql.mysql.AbstractSqlTable; import com.bencodez.simpleapi.sql.mysql.DbType; @@ -30,7 +31,8 @@ public String buildCreateTableSql(DbType dbType) { + qi("id") + " BIGSERIAL PRIMARY KEY, " + qi("playerName") + " VARCHAR(100), " + qi("service") + " VARCHAR(100), " - + qi("time") + " BIGINT" + + qi("time") + " BIGINT, " + + qi("voteId") + " VARCHAR(36)" + ");"; } @@ -39,6 +41,7 @@ public String buildCreateTableSql(DbType dbType) { + qi("playerName") + " VARCHAR(100)," + qi("service") + " VARCHAR(100)," + qi("time") + " BIGINT," + + qi("voteId") + " VARCHAR(36)," + "INDEX idx_time (" + qi("time") + ")" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; } @@ -62,6 +65,7 @@ public ProxyTimedVoteCacheTable(MySQL existingMysql, String tablePrefix, boolean super((tablePrefix != null ? tablePrefix : "") + "votingplugin_timedvotecache", existingMysql, debug); + ensureVoteIdColumn(); ensureIndexes(); } @@ -72,9 +76,28 @@ public ProxyTimedVoteCacheTable(MySQL existingMysql, String tablePrefix, boolean */ public ProxyTimedVoteCacheTable(MysqlConfig config, boolean debug) { super("votingplugin_timedvotecache", config, debug); + ensureVoteIdColumn(); ensureIndexes(); } + private void ensureVoteIdColumn() { + String probeSql = "SELECT " + qi("voteId") + " FROM " + qi(getTableName()) + " WHERE 1 = 0;"; + try (Connection conn = mysql.getConnectionManager().getConnection(); + PreparedStatement ps = conn.prepareStatement(probeSql)) { + ps.executeQuery(); + return; + } catch (SQLException ignored) { + // Column does not exist yet. + } + + try { + new Query(mysql, "ALTER TABLE " + qi(getTableName()) + " ADD COLUMN " + qi("voteId") + + " VARCHAR(36);").executeUpdate(); + } catch (SQLException e) { + debug(e); + } + } + private void ensureIndexes() { if (getDbType() == DbType.POSTGRESQL) { try { @@ -89,18 +112,20 @@ private void ensureIndexes() { // --- INSERT --- /** * Inserts a timed vote. + * @param voteId unique vote identifier * @param playerName the player name * @param service the voting service * @param time the vote time */ - public void insertTimedVote(String playerName, String service, long time) { + public void insertTimedVote(UUID voteId, String playerName, String service, long time) { String sql = "INSERT INTO " + qi(getTableName()) + " (" + qi("playerName") + ", " + qi("service") + ", " - + qi("time") + ") VALUES (?, ?, ?);"; + + qi("time") + ", " + qi("voteId") + ") VALUES (?, ?, ?, ?);"; try (Connection conn = mysql.getConnectionManager().getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, playerName); ps.setString(2, service); ps.setLong(3, time); + ps.setString(4, voteId == null ? null : voteId.toString()); ps.executeUpdate(); } catch (SQLException e) { debug(e); @@ -189,7 +214,8 @@ private List selectVotes(String sql, Object[] params) { rs.getInt("id"), rs.getString("playerName"), rs.getString("service"), - rs.getLong("time") + rs.getLong("time"), + parseUuid(rs.getString("voteId")) )); } } @@ -199,6 +225,17 @@ private List selectVotes(String sql, Object[] params) { return list; } + private UUID parseUuid(String value) { + if (value == null || value.isEmpty()) { + return null; + } + try { + return UUID.fromString(value); + } catch (IllegalArgumentException ignored) { + return null; + } + } + /** * Represents a row in the timed vote cache table. */ @@ -207,6 +244,7 @@ public static class TimedVoteRow { private final String playerName; private final String service; private final long time; + private final UUID voteId; /** * Constructor for TimedVoteRow. @@ -214,12 +252,14 @@ public static class TimedVoteRow { * @param playerName the player name * @param service the voting service * @param time the vote time + * @param voteId unique vote identifier */ - public TimedVoteRow(int id, String playerName, String service, long time) { + public TimedVoteRow(int id, String playerName, String service, long time, UUID voteId) { this.id = id; this.playerName = playerName; this.service = service; this.time = time; + this.voteId = voteId; } /** @@ -253,5 +293,14 @@ public String getService() { public long getTime() { return time; } + + /** + * Gets the vote identifier. + * + * @return vote identifier or null + */ + public UUID getVoteId() { + return voteId; + } } } diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/VoteCacheHandler.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/VoteCacheHandler.java index 9ea53e30a..90889727c 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/VoteCacheHandler.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/cache/VoteCacheHandler.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Queue; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -88,6 +89,11 @@ public int getProxyCachedTotal(String uuid) { * @param vote the vote to add */ public void addServerVote(String server, OfflineBungeeVote vote) { + if (containsServerVote(server, vote.getVoteId())) { + debug1("Not caching duplicate vote " + vote.getVoteId() + " for server " + server); + return; + } + cachedVotes.putIfAbsent(server, new ArrayList<>()); cachedVotes.get(server).add(vote); @@ -160,6 +166,11 @@ public ArrayList getOnlineVotes(String uuid) { * @param vote the vote to add */ public void addOnlineVote(String uuid, OfflineBungeeVote vote) { + if (containsOnlineVote(uuid, vote.getVoteId())) { + debug1("Not caching duplicate online vote " + vote.getVoteId() + " for " + uuid); + return; + } + cachedOnlineVotes.putIfAbsent(uuid, new ArrayList<>()); cachedOnlineVotes.get(uuid).add(vote); @@ -226,6 +237,82 @@ public void checkVoteCacheTime(int voteCacheTime) { } } + /** + * Checks whether a vote is already cached for a server. + * + * @param server target server + * @param voteId unique vote identifier + * @return true if the vote is already cached for the server + */ + private boolean containsServerVote(String server, UUID voteId) { + if (voteId == null) { + return false; + } + for (OfflineBungeeVote vote : getVotes(server)) { + if (voteId.equals(vote.getVoteId())) { + return true; + } + } + return false; + } + + /** + * Checks whether a vote is already cached for an online player. + * + * @param uuid player UUID + * @param voteId unique vote identifier + * @return true if the vote is already cached for the player + */ + private boolean containsOnlineVote(String uuid, UUID voteId) { + if (voteId == null) { + return false; + } + for (OfflineBungeeVote vote : getOnlineVotes(uuid)) { + if (voteId.equals(vote.getVoteId())) { + return true; + } + } + return false; + } + + /** + * Reads a vote identifier using the current key and the legacy key. + * + * @param data cached vote data + * @return stored vote identifier or an empty string + */ + private String readVoteId(DataNode data) { + if (data.has("VoteId")) { + return data.get("VoteId").asString(); + } + if (data.has("VoteID")) { + return data.get("VoteID").asString(); + } + return ""; + } + + /** + * Reads an optional UUID from cached data. + * + * @param data cached data + * @param key value key + * @return parsed UUID or null + */ + private UUID readUuid(DataNode data, String key) { + if (!data.has(key)) { + return null; + } + String value = data.get(key).asString(); + if (value == null || value.isEmpty()) { + return null; + } + try { + return UUID.fromString(value); + } catch (IllegalArgumentException ignored) { + return null; + } + } + /** * Saves the vote cache to storage. */ @@ -234,7 +321,7 @@ public void saveVoteCache() { if (!getTimeChangeQueue().isEmpty()) { for (VoteTimeQueue vote : getTimeChangeQueue()) { - timedVoteCacheTable.insertTimedVote(vote.getName(), vote.getService(), vote.getTime()); + timedVoteCacheTable.insertTimedVote(vote.getVoteId(), vote.getName(), vote.getService(), vote.getTime()); } } } else { @@ -286,8 +373,8 @@ public void load() { // Load timed votes from MySQL ArrayList timedVotes = new ArrayList<>(); timedVoteCacheTable.getAllVotes().forEach(timedVoteRow -> { - VoteTimeQueue voteTimeQueue = new VoteTimeQueue(timedVoteRow.getPlayerName(), timedVoteRow.getService(), - timedVoteRow.getTime()); + VoteTimeQueue voteTimeQueue = new VoteTimeQueue(timedVoteRow.getVoteId(), timedVoteRow.getPlayerName(), + timedVoteRow.getService(), timedVoteRow.getTime()); timedVotes.add(voteTimeQueue); }); timeChangeQueue.addAll(timedVotes); @@ -303,8 +390,9 @@ public void load() { String name = data.has("Name") ? data.get("Name").asString() : ""; String service = data.has("Service") ? data.get("Service").asString() : ""; long time = data.has("Time") ? data.get("Time").asLong() : 0L; + UUID voteId = readUuid(data, "VoteId"); - getTimeChangeQueue().add(new VoteTimeQueue(name, service, time)); + getTimeChangeQueue().add(new VoteTimeQueue(voteId, name, service, time)); } } @@ -326,7 +414,7 @@ public void load() { long time = data.has("Time") ? data.get("Time").asLong() : 0L; boolean real = data.has("Real") && data.get("Real").asBoolean(); String text = data.has("Text") ? data.get("Text").asString() : ""; - String voteId = data.has("VoteId") ? data.get("VoteId").asString() : ""; + String voteId = readVoteId(data); votes.add(new OfflineBungeeVote(voteId, name, uuid, service, time, real, text)); } @@ -351,7 +439,7 @@ public void load() { long time = data.has("Time") ? data.get("Time").asLong() : 0L; boolean real = data.has("Real") && data.get("Real").asBoolean(); String text = data.has("Text") ? data.get("Text").asString() : ""; - String voteId = data.has("VoteId") ? data.get("VoteId").asString() : ""; + String voteId = readVoteId(data); votes.add(new OfflineBungeeVote(voteId, name, uuid, service, time, real, text)); } diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/velocity/VelocityJsonVoteCache.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/velocity/VelocityJsonVoteCache.java index e20b03202..3771afebd 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/velocity/VelocityJsonVoteCache.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/velocity/VelocityJsonVoteCache.java @@ -28,6 +28,8 @@ public void addTimedVote(int num, VoteTimeQueue voteTimedQueue) { setPath(voteTimedQueue.getName(), "TimedVoteCache", String.valueOf(num), "Name"); setPath(voteTimedQueue.getService(), "TimedVoteCache", String.valueOf(num), "Service"); setPath(voteTimedQueue.getTime(), "TimedVoteCache", String.valueOf(num), "Time"); + setPath(voteTimedQueue.getVoteId() == null ? null : voteTimedQueue.getVoteId().toString(), "TimedVoteCache", + String.valueOf(num), "VoteId"); } @Override @@ -38,7 +40,7 @@ public void addVote(String server, int num, OfflineBungeeVote voteData) { setPath(voteData.getTime(), "VoteCache", server, String.valueOf(num), "Time"); setPath(voteData.isRealVote(), "VoteCache", server, String.valueOf(num), "Real"); setPath(voteData.getText(), "VoteCache", server, String.valueOf(num), "Text"); - setPath(voteData.getVoteId() != null ? voteData.getVoteId().toString() : null, "VoteCache", server, String.valueOf(num), "VoteID"); + setPath(voteData.getVoteId() != null ? voteData.getVoteId().toString() : null, "VoteCache", server, String.valueOf(num), "VoteId"); } @Override @@ -49,7 +51,7 @@ public void addVoteOnline(String player, int num, OfflineBungeeVote voteData) { setPath(voteData.getTime(), "OnlineCache", player, String.valueOf(num), "Time"); setPath(voteData.isRealVote(), "OnlineCache", player, String.valueOf(num), "Real"); setPath(voteData.getText(), "OnlineCache", player, String.valueOf(num), "Text"); - setPath(voteData.getVoteId() != null ? voteData.getVoteId().toString() : null, "OnlineCache", player, String.valueOf(num), "VoteID"); + setPath(voteData.getVoteId() != null ? voteData.getVoteId().toString() : null, "OnlineCache", player, String.valueOf(num), "VoteId"); } @Override diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/timequeue/VoteTimeQueue.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/timequeue/VoteTimeQueue.java index bdf08a3a8..29f1eda5a 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/timequeue/VoteTimeQueue.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/timequeue/VoteTimeQueue.java @@ -1,8 +1,13 @@ package com.bencodez.votingplugin.timequeue; +import java.util.UUID; + import lombok.Getter; import lombok.Setter; +/** + * Represents a vote delayed while a proxy time change is active. + */ public class VoteTimeQueue { @Getter @Setter @@ -13,8 +18,31 @@ public class VoteTimeQueue { @Getter @Setter private long time; + @Getter + @Setter + private UUID voteId; + /** + * Creates a legacy-compatible queued vote without an identifier. + * + * @param name player name + * @param service service site + * @param time vote timestamp + */ public VoteTimeQueue(String name, String service, long time) { + this(null, name, service, time); + } + + /** + * Creates a queued vote with its original identifier. + * + * @param voteId unique vote identifier + * @param name player name + * @param service service site + * @param time vote timestamp + */ + public VoteTimeQueue(UUID voteId, String name, String service, long time) { + this.voteId = voteId; this.name = name; this.service = service; this.time = time; diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VoteCacheHandlerVoteIdTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VoteCacheHandlerVoteIdTest.java new file mode 100644 index 000000000..64a11d7a1 --- /dev/null +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/VoteCacheHandlerVoteIdTest.java @@ -0,0 +1,189 @@ +package com.bencodez.votingplugin.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.bencodez.votingplugin.proxy.OfflineBungeeVote; +import com.bencodez.votingplugin.proxy.cache.DataNode; +import com.bencodez.votingplugin.proxy.cache.IVoteCache; +import com.bencodez.votingplugin.proxy.cache.VoteCacheHandler; +import com.bencodez.votingplugin.timequeue.VoteTimeQueue; + +/** + * Regression tests for proxy vote identity across caches and delayed processing. + */ +public class VoteCacheHandlerVoteIdTest { + + private IVoteCache storage; + private VoteCacheHandler handler; + + @BeforeEach + public void setUp() { + storage = mock(IVoteCache.class); + when(storage.getServerVotes("server")).thenReturn(Collections.emptyList()); + when(storage.getOnlineVotes("player-uuid")).thenReturn(Collections.emptyList()); + handler = newHandler(storage); + } + + @Test + public void serverCacheRejectsDuplicateVoteId() { + UUID voteId = UUID.randomUUID(); + + handler.addServerVote("server", vote(voteId, 100L)); + handler.addServerVote("server", vote(voteId, 101L)); + + assertEquals(1, handler.getVotes("server").size()); + assertEquals(100L, handler.getVotes("server").get(0).getTime()); + verify(storage).addVote("server", 0, handler.getVotes("server").get(0)); + } + + @Test + public void onlineCacheRejectsDuplicateVoteId() { + UUID voteId = UUID.randomUUID(); + + handler.addOnlineVote("player-uuid", vote(voteId, 100L)); + handler.addOnlineVote("player-uuid", vote(voteId, 101L)); + + assertEquals(1, handler.getOnlineVotes("player-uuid").size()); + verify(storage).addVoteOnline("player-uuid", 0, handler.getOnlineVotes("player-uuid").get(0)); + } + + @Test + public void distinctVoteIdsAreNotCollapsed() { + handler.addServerVote("server", vote(UUID.randomUUID(), 100L)); + handler.addServerVote("server", vote(UUID.randomUUID(), 101L)); + + assertEquals(2, handler.getVotes("server").size()); + } + + @Test + public void missingVoteIdsRemainLegacyCompatible() { + handler.addServerVote("server", vote(null, 100L)); + handler.addServerVote("server", vote(null, 101L)); + + assertEquals(2, handler.getVotes("server").size()); + } + + @Test + public void currentVoteIdKeyLoadsFromJsonCache() { + UUID voteId = UUID.randomUUID(); + handler = handlerForStoredVote("VoteId", voteId); + + handler.load(); + + assertEquals(voteId, handler.getVotes("server").get(0).getVoteId()); + } + + @Test + public void legacyVoteIdKeyLoadsFromJsonCache() { + UUID voteId = UUID.randomUUID(); + handler = handlerForStoredVote("VoteID", voteId); + + handler.load(); + + assertEquals(voteId, handler.getVotes("server").get(0).getVoteId()); + } + + @Test + public void timedVoteIdIsPassedToJsonStorage() { + UUID voteId = UUID.randomUUID(); + VoteTimeQueue queued = new VoteTimeQueue(voteId, "Player", "Service", 100L); + handler.addTimeVoteToCache(queued); + + handler.saveVoteCache(); + + verify(storage).addTimedVote(eq(0), same(queued)); + verify(storage).save(); + assertEquals(voteId, handler.getTimeChangeQueue().element().getVoteId()); + } + + @Test + public void legacyTimedVoteConstructorHasNoId() { + assertNull(new VoteTimeQueue("Player", "Service", 100L).getVoteId()); + } + + private VoteCacheHandler handlerForStoredVote(String idKey, UUID voteId) { + IVoteCache stored = mock(IVoteCache.class); + DataNode voteNode = mock(DataNode.class); + when(stored.getTimedVoteCache()).thenReturn(Collections.emptyList()); + when(stored.getServers()).thenReturn(List.of("server")); + when(stored.getServerVotes("server")).thenReturn(List.of("0")); + when(stored.getServerVotes("server", "0")).thenReturn(voteNode); + when(stored.getPlayers()).thenReturn(Collections.emptyList()); + when(voteNode.isObject()).thenReturn(true); + + stubString(voteNode, "Name", "Player"); + stubString(voteNode, "UUID", "player-uuid"); + stubString(voteNode, "Service", "Service"); + stubLong(voteNode, "Time", 100L); + stubBoolean(voteNode, "Real", true); + stubString(voteNode, "Text", "totals"); + stubString(voteNode, idKey, voteId.toString()); + if ("VoteID".equals(idKey)) { + when(voteNode.has("VoteId")).thenReturn(false); + } + + return newHandler(stored); + } + + private static void stubString(DataNode parent, String key, String value) { + DataNode child = mock(DataNode.class); + when(parent.has(key)).thenReturn(true); + when(parent.get(key)).thenReturn(child); + when(child.asString()).thenReturn(value); + } + + private static void stubLong(DataNode parent, String key, long value) { + DataNode child = mock(DataNode.class); + when(parent.has(key)).thenReturn(true); + when(parent.get(key)).thenReturn(child); + when(child.asLong()).thenReturn(value); + } + + private static void stubBoolean(DataNode parent, String key, boolean value) { + DataNode child = mock(DataNode.class); + when(parent.has(key)).thenReturn(true); + when(parent.get(key)).thenReturn(child); + when(child.asBoolean()).thenReturn(value); + } + + private static OfflineBungeeVote vote(UUID voteId, long time) { + return new OfflineBungeeVote(voteId, "Player", "player-uuid", "Service", time, true, "totals"); + } + + private static VoteCacheHandler newHandler(IVoteCache storage) { + return new VoteCacheHandler(null, false, false, null, false, storage) { + @Override + public void logInfo1(String msg) { + } + + @Override + public void logSevere1(String msg) { + } + + @Override + public void debug1(Exception e) { + } + + @Override + public void debug1(Throwable e) { + } + + @Override + public void debug1(String msg) { + } + }; + } +}