diff --git a/.gitignore b/.gitignore
index bf178ec..1fe2b8c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ dist/
*.iml
.vscode/
.DS_Store
+MSP3/
diff --git a/pom.xml b/pom.xml
index 68b6a65..839051c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,6 +37,8 @@
UTF-8
5.1.0
8.0.33
+ 5.10.2
+ 5.11.0
@@ -63,6 +65,14 @@
maven-shade-plugin
3.5.1
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+ -Dnet.bytebuddy.experimental=true
+
+
diff --git a/spigot/pom.xml b/spigot/pom.xml
index 062042d..0a84672 100644
--- a/spigot/pom.xml
+++ b/spigot/pom.xml
@@ -42,6 +42,18 @@
2.11.6
provided
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
diff --git a/spigot/src/test/java/fr/votenetwork/spigot/VoteConfigTest.java b/spigot/src/test/java/fr/votenetwork/spigot/VoteConfigTest.java
new file mode 100644
index 0000000..c5c7939
--- /dev/null
+++ b/spigot/src/test/java/fr/votenetwork/spigot/VoteConfigTest.java
@@ -0,0 +1,70 @@
+package fr.votenetwork.spigot;
+
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class VoteConfigTest {
+
+ @Test
+ void defaultsAreUsedWhenKeysAbsent() {
+ VoteNetworkSpigot plugin = mock(VoteNetworkSpigot.class);
+ when(plugin.getConfig()).thenReturn(new YamlConfiguration());
+
+ VoteConfig config = new VoteConfig(plugin);
+
+ assertTrue(config.isMysqlStorage());
+ assertEquals("127.0.0.1", config.getMysqlHost());
+ assertEquals(3306, config.getMysqlPort());
+ assertEquals("votenetwork", config.getMysqlDatabase());
+ assertEquals(100, config.getVotePartyVotesRequired());
+ assertFalse(config.isPerServerScope());
+ assertFalse(config.isVoteBroadcastEnabled());
+ }
+
+ @Test
+ void mysqlStorageIsFalseOnlyForYaml() {
+ YamlConfiguration yaml = new YamlConfiguration();
+ yaml.set("storage.type", "yaml");
+ VoteNetworkSpigot plugin = mock(VoteNetworkSpigot.class);
+ when(plugin.getConfig()).thenReturn(yaml);
+
+ VoteConfig config = new VoteConfig(plugin);
+
+ assertFalse(config.isMysqlStorage());
+ assertEquals("yaml", config.getStorageType());
+ }
+
+ @Test
+ void perServerScopeReadFromConfig() {
+ YamlConfiguration yaml = new YamlConfiguration();
+ yaml.set("storage.pending-votes-scope", "per-server");
+ yaml.set("server-name", "gen1");
+ VoteNetworkSpigot plugin = mock(VoteNetworkSpigot.class);
+ when(plugin.getConfig()).thenReturn(yaml);
+
+ VoteConfig config = new VoteConfig(plugin);
+
+ assertTrue(config.isPerServerScope());
+ assertEquals("gen1", config.getServerName());
+ }
+
+ @Test
+ void messagesAreColorizedAndPlaceholdersPreserved() {
+ YamlConfiguration yaml = new YamlConfiguration();
+ yaml.set("messages.claim-success", "&aTu as recupere &e%amount% &avotes !");
+ VoteNetworkSpigot plugin = mock(VoteNetworkSpigot.class);
+ when(plugin.getConfig()).thenReturn(yaml);
+
+ VoteConfig config = new VoteConfig(plugin);
+ String message = config.getMessageClaimSuccess();
+
+ assertFalse(message.contains("&a"), "les codes couleur '&' doivent etre traduits");
+ assertTrue(message.contains("%amount%"), "le placeholder %amount% doit rester intact pour etre remplace plus tard");
+ }
+}
diff --git a/spigot/src/test/java/fr/votenetwork/spigot/storage/YamlPendingVoteStoreTest.java b/spigot/src/test/java/fr/votenetwork/spigot/storage/YamlPendingVoteStoreTest.java
new file mode 100644
index 0000000..d8f0b4c
--- /dev/null
+++ b/spigot/src/test/java/fr/votenetwork/spigot/storage/YamlPendingVoteStoreTest.java
@@ -0,0 +1,67 @@
+package fr.votenetwork.spigot.storage;
+
+import fr.votenetwork.spigot.VoteNetworkSpigot;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class YamlPendingVoteStoreTest {
+
+ @Test
+ void peekReturnsZeroWhenNothingStored(@TempDir File dataFolder) {
+ YamlPendingVoteStore store = newStore(dataFolder);
+ UUID uuid = UUID.randomUUID();
+
+ AtomicInteger result = new AtomicInteger(-1);
+ store.peek(uuid, result::set, e -> { throw new AssertionError(e); });
+
+ assertEquals(0, result.get());
+ }
+
+ @Test
+ void fetchAndClearResetsCounterToZeroAfterReading(@TempDir File dataFolder) throws Exception {
+ YamlPendingVoteStore store = newStore(dataFolder);
+ UUID uuid = UUID.randomUUID();
+
+ writeInitialVotes(dataFolder, uuid, 3);
+
+ AtomicInteger firstRead = new AtomicInteger(-1);
+ store.fetchAndClear(uuid, firstRead::set, e -> { throw new AssertionError(e); });
+ assertEquals(3, firstRead.get());
+
+ AtomicInteger secondRead = new AtomicInteger(-1);
+ store.peek(uuid, secondRead::set, e -> { throw new AssertionError(e); });
+ assertEquals(0, secondRead.get(), "fetchAndClear doit remettre le compteur a 0");
+ }
+
+ @Test
+ void testConnectionReportsWritableDirectory(@TempDir File dataFolder) {
+ YamlPendingVoteStore store = newStore(dataFolder);
+
+ AtomicReference success = new AtomicReference<>();
+ store.testConnection((ok, message) -> success.set(ok));
+
+ assertEquals(Boolean.TRUE, success.get(), "un dossier temporaire accessible en ecriture doit etre rapporte comme fonctionnel");
+ }
+
+ private YamlPendingVoteStore newStore(File dataFolder) {
+ VoteNetworkSpigot plugin = mock(VoteNetworkSpigot.class);
+ when(plugin.getDataFolder()).thenReturn(dataFolder);
+ return new YamlPendingVoteStore(plugin);
+ }
+
+ private void writeInitialVotes(File dataFolder, UUID uuid, int votes) throws Exception {
+ org.bukkit.configuration.file.YamlConfiguration yaml = new org.bukkit.configuration.file.YamlConfiguration();
+ yaml.set("players." + uuid + ".votes", votes);
+ File file = new File(dataFolder, "pending-votes.yml");
+ yaml.save(file);
+ }
+}
diff --git a/velocity/pom.xml b/velocity/pom.xml
index 00d22f3..0c50857 100644
--- a/velocity/pom.xml
+++ b/velocity/pom.xml
@@ -36,6 +36,12 @@
2.10.1
provided
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
diff --git a/velocity/src/test/java/fr/votenetwork/velocity/VoteConfigTest.java b/velocity/src/test/java/fr/votenetwork/velocity/VoteConfigTest.java
new file mode 100644
index 0000000..1b62af7
--- /dev/null
+++ b/velocity/src/test/java/fr/votenetwork/velocity/VoteConfigTest.java
@@ -0,0 +1,104 @@
+package fr.votenetwork.velocity;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.helpers.NOPLogger;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class VoteConfigTest {
+
+ @Test
+ void loadCopiesDefaultsWhenFileMissing(@TempDir Path dataDir) {
+ VoteConfig config = new VoteConfig(dataDir, NOPLogger.NOP_LOGGER);
+ config.load();
+
+ assertTrue(config.isMysqlStorage());
+ assertEquals("jdbc:mysql://127.0.0.1:3306/votenetwork?useSSL=false&autoReconnect=true&characterEncoding=utf8",
+ config.getJdbcUrl());
+ assertTrue(Files.exists(dataDir.resolve("config.properties")));
+ }
+
+ @Test
+ void perServerScopeFallsBackToGlobalWhenServerListEmpty(@TempDir Path dataDir) throws IOException {
+ writeConfig(dataDir, """
+ storage.type=mysql
+ mysql.host=127.0.0.1
+ mysql.port=3306
+ mysql.database=votenetwork
+ mysql.user=root
+ mysql.password=changeme
+ mysql.pool-size=5
+ direct-vote.servers=
+ pending-votes.scope=per-server
+ """);
+
+ VoteConfig config = new VoteConfig(dataDir, NOPLogger.NOP_LOGGER);
+ config.load();
+
+ assertFalse(config.isPerServerScope(), "sans direct-vote.servers, per-server doit retomber sur global");
+ }
+
+ @Test
+ void perServerScopeAppliesWhenServersListed(@TempDir Path dataDir) throws IOException {
+ writeConfig(dataDir, """
+ storage.type=mysql
+ mysql.host=127.0.0.1
+ mysql.port=3306
+ mysql.database=votenetwork
+ mysql.user=root
+ mysql.password=changeme
+ mysql.pool-size=5
+ direct-vote.servers=gen1, GEN2
+ pending-votes.scope=per-server
+ """);
+
+ VoteConfig config = new VoteConfig(dataDir, NOPLogger.NOP_LOGGER);
+ config.load();
+
+ assertTrue(config.isPerServerScope());
+ assertTrue(config.isDirectVoteServer("gen1"));
+ assertTrue(config.isDirectVoteServer("Gen2"), "la comparaison doit ignorer la casse");
+ assertFalse(config.isDirectVoteServer("lobby"));
+ }
+
+ @Test
+ void isDirectVoteServerAllowsAllWhenListEmpty(@TempDir Path dataDir) throws IOException {
+ writeConfig(dataDir, """
+ storage.type=mysql
+ direct-vote.servers=
+ """);
+
+ VoteConfig config = new VoteConfig(dataDir, NOPLogger.NOP_LOGGER);
+ config.load();
+
+ assertTrue(config.isDirectVoteServer("n_importe_quoi"));
+ }
+
+ @Test
+ void mergeAddsMissingKeysWithoutTouchingExistingOnes(@TempDir Path dataDir) throws IOException {
+ writeConfig(dataDir, """
+ mysql.password=ne-pas-toucher
+ """);
+
+ VoteConfig config = new VoteConfig(dataDir, NOPLogger.NOP_LOGGER);
+ config.load();
+
+ String fileContent = Files.readString(dataDir.resolve("config.properties"), StandardCharsets.UTF_8);
+ assertTrue(fileContent.contains("mysql.password=ne-pas-toucher"), "la valeur existante ne doit jamais etre ecrasee");
+ assertTrue(fileContent.contains("pending-votes.scope="), "les cles manquantes doivent etre ajoutees");
+ assertEquals("ne-pas-toucher", config.getPassword());
+ }
+
+ private void writeConfig(Path dataDir, String content) throws IOException {
+ Files.createDirectories(dataDir);
+ Files.writeString(dataDir.resolve("config.properties"), content, StandardCharsets.UTF_8);
+ }
+}
diff --git a/velocity/src/test/java/fr/votenetwork/velocity/YamlPendingVoteStoreTest.java b/velocity/src/test/java/fr/votenetwork/velocity/YamlPendingVoteStoreTest.java
new file mode 100644
index 0000000..6fa86a2
--- /dev/null
+++ b/velocity/src/test/java/fr/votenetwork/velocity/YamlPendingVoteStoreTest.java
@@ -0,0 +1,55 @@
+package fr.votenetwork.velocity;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.helpers.NOPLogger;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class YamlPendingVoteStoreTest {
+
+ @Test
+ void addPendingVoteCreatesEntryAndIncrements(@TempDir Path dataDir)
+ throws ExecutionException, InterruptedException, TimeoutException, IOException {
+ YamlPendingVoteStore store = new YamlPendingVoteStore(dataDir, NOPLogger.NOP_LOGGER);
+ UUID uuid = UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5");
+
+ store.addPendingVote(uuid, "Notch").get(5, TimeUnit.SECONDS);
+
+ String content = Files.readString(dataDir.resolve("pending-votes.yml"), StandardCharsets.UTF_8);
+ assertTrue(content.contains(uuid.toString()));
+ assertTrue(content.contains("name: Notch"));
+ assertTrue(content.contains("votes: 1"));
+
+ store.addPendingVote(uuid, "Notch").get(5, TimeUnit.SECONDS);
+
+ content = Files.readString(dataDir.resolve("pending-votes.yml"), StandardCharsets.UTF_8);
+ assertTrue(content.contains("votes: 2"), "un deuxieme vote hors ligne doit incrementer le compteur existant");
+ }
+
+ @Test
+ void addPendingVoteHandlesTwoDifferentPlayersIndependently(@TempDir Path dataDir)
+ throws ExecutionException, InterruptedException, TimeoutException, IOException {
+ YamlPendingVoteStore store = new YamlPendingVoteStore(dataDir, NOPLogger.NOP_LOGGER);
+ UUID uuidA = UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5");
+ UUID uuidB = UUID.fromString("853c80ef-3c37-49fd-aa49-938b674adae6");
+
+ store.addPendingVote(uuidA, "Sar_Tron").get(5, TimeUnit.SECONDS);
+ store.addPendingVote(uuidB, "Notch").get(5, TimeUnit.SECONDS);
+
+ String content = Files.readString(dataDir.resolve("pending-votes.yml"), StandardCharsets.UTF_8);
+ assertTrue(content.contains(uuidA.toString()));
+ assertTrue(content.contains(uuidB.toString()));
+ assertTrue(content.contains("name: Sar_Tron"));
+ assertTrue(content.contains("name: Notch"));
+ }
+}