8 Commits
Author SHA1 Message Date
SarTron-NorthBlueandClaude Sonnet 5 964f375841 Ajoute le GUI VoteParty avec liens de vote cliquables (v1.0.1)
CI / build (push) Successful in 12m13s
Nouvelle commande /votegui : affiche la progression du VoteParty et un
item par site de vote configure. Un clic ferme le GUI et envoie un
message chat cliquable vers l'URL du site (Bukkit ne permet pas
d'ouvrir un navigateur directement depuis un clic d'inventaire).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 13:26:53 +04:00
SarTron-NorthBlueandClaude Sonnet 5 35a8425254 Fix CI : downgrade upload-artifact v4 -> v3 (incompatible avec Gitea)
CI / build (push) Successful in 12m9s
Le build et les tests passaient deja (Setup JDK, checkout, mvn verify
tous verts). Seul l'upload d'artefact echouait : upload-artifact@v4
utilise la nouvelle API @actions/artifact v2, pas encore supportee par
le backend Gitea Actions (GHESNotSupportedError). v3 utilise l'ancienne
API, compatible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 09:24:23 +04:00
SarTron-NorthBlueandClaude Sonnet 5 5a7aec5344 Fix CI : revient sur ubuntu-latest, installe Maven via apt-get
CI / build (push) Failing after 12m37s
Le container maven:3.9-eclipse-temurin-17 n'a pas Node.js, requis par
les actions JS (checkout, cache, upload-artifact) -> echec au premier
step. Retour au runner par defaut + installation explicite de Maven
(actions/setup-java installe le JDK mais jamais Maven).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 09:10:43 +04:00
SarTron-NorthBlueandClaude Sonnet 5 686cb4de26 Fix CI : mvn introuvable sur le runner (image sans Maven preinstalle)
CI / build (push) Failing after 1m8s
actions/setup-java installe le JDK mais pas Maven. Ca marche sur les
runners GitHub-hostes (Maven deja present dans l'image), mais pas sur
ce runner self-hoste (act) qui utilise une image minimale. Bascule
sur l'image Docker officielle maven:3.9-eclipse-temurin-17, qui a
Maven + JDK 17 preinstalles, peu importe le runner sous-jacent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 09:08:16 +04:00
SarTron-NorthBlueandClaude Sonnet 5 70d41af369 ci: declenche manuellement le pipeline pour verification
CI / build (push) Failing after 4m49s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 09:02:11 +04:00
SarTron-NorthBlueandClaude Sonnet 5 3a84f16f23 Ajoute le pipeline CI Gitea Actions (CP11 - demarche DevOps)
CI / build (push) Failing after 5m24s
mvn clean verify (compile + tests) a chaque push/PR sur main, upload
des jars en artefact si succes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 09:18:32 +04:00
SarTron-NorthBlueandClaude Sonnet 5 5abe49fdd0 Ajoute des tests unitaires JUnit 5 (CP9 - plans de tests)
14 tests couvrant VoteConfig (Spigot + Velocity) et YamlPendingVoteStore
(Spigot + Velocity) : valeurs par defaut, portee per-server/global,
merge de config sans ecraser l'existant, incrementation/reset des
votes en attente, colorisation des messages.

Mockito pour VoteConfig/YamlPendingVoteStore cote Spigot (mock de
VoteNetworkSpigot, pas besoin de serveur Bukkit reel). Cote Velocity,
tests directs sans framework (classes deja decouplees de Velocity-API).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 09:14:12 +04:00
SarTron-NorthBlueandClaude Sonnet 5 d5f6e0cfb3 Ajoute un script de release Gitea (scripts/release.sh)
Sur demande uniquement (pas de trigger auto par commit) : build les 2
jars, zip, cree le tag/release via l'API Gitea et y attache le zip.
Token passe en variable d'env GITEA_TOKEN, jamais stocke dans le repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:24:13 +04:00
21 changed files with 744 additions and 4 deletions
+38
View File
@@ -0,0 +1,38 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: maven
- name: Install Maven
run: |
which mvn || (sudo apt-get update && sudo apt-get install -y maven)
- name: Build and test
run: mvn -B clean verify
- name: Upload jars
if: success()
uses: actions/upload-artifact@v3
with:
name: votenetwork-jars
path: |
velocity/target/VoteNetwork-Velocity.jar
spigot/target/VoteNetwork-Spigot.jar
retention-days: 14
+1
View File
@@ -5,3 +5,4 @@ dist/
*.iml
.vscode/
.DS_Store
MSP3/
+8
View File
@@ -44,6 +44,14 @@ mvn clean package
Jars produits dans `velocity/target/` et `spigot/target/`.
### Publier une release (Gitea)
```
GITEA_TOKEN=xxxx ./scripts/release.sh 1.0.1 "Description de la release"
```
Build les 2 jars, les zip dans `dist/VoteNetwork.zip`, crée le tag/release sur Gitea et y attache le zip. Nécessite un token Gitea (Paramètres → Applications → Générer un nouveau jeton, scope `repo`), passé uniquement en variable d'environnement — jamais commité.
## Configuration
Chaque composant a son propre fichier, tous deux dans un dossier **`votenetwork`** :
+11 -1
View File
@@ -6,7 +6,7 @@
<groupId>fr.votenetwork</groupId>
<artifactId>votenetwork-parent</artifactId>
<version>1.0.0</version>
<version>1.0.1</version>
<packaging>pom</packaging>
<modules>
@@ -37,6 +37,8 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hikaricp.version>5.1.0</hikaricp.version>
<mysql.version>8.0.33</mysql.version>
<junit.version>5.10.2</junit.version>
<mockito.version>5.11.0</mockito.version>
</properties>
<repositories>
@@ -63,6 +65,14 @@
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<argLine>-Dnet.bytebuddy.experimental=true</argLine>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Build les 2 jars, zip, puis cree une release Gitea avec le zip attache.
#
# Usage :
# GITEA_TOKEN=xxxx ./scripts/release.sh 1.0.1 "Description de la release"
#
# Le token doit avoir le scope "repo" (Parametres -> Applications -> Generer un nouveau jeton).
# Ne JAMAIS commit le token. Passe-le uniquement en variable d'environnement.
set -euo pipefail
VERSION="${1:?Usage: release.sh <version> [notes]}"
NOTES="${2:-Release ${VERSION}}"
GITEA_HOST="https://gitea.louconnect.fr"
OWNER="Sar_Tron"
REPO="VoteNetwork"
TAG="v${VERSION}"
if [ -z "${GITEA_TOKEN:-}" ]; then
echo "Erreur : variable GITEA_TOKEN non definie." >&2
echo "Cree un token dans Gitea (Parametres > Applications, scope repo) puis :" >&2
echo " GITEA_TOKEN=xxxx ./scripts/release.sh ${VERSION}" >&2
exit 1
fi
cd "$(dirname "$0")/.."
echo "==> Build Maven"
mvn -q clean package
echo "==> Preparation du zip de distribution"
rm -rf dist
mkdir -p dist
cp velocity/target/VoteNetwork-Velocity.jar dist/
cp spigot/target/VoteNetwork-Spigot.jar dist/
if command -v zip >/dev/null 2>&1; then
(cd dist && zip -q VoteNetwork.zip VoteNetwork-Velocity.jar VoteNetwork-Spigot.jar)
else
powershell -Command "Compress-Archive -Path dist/VoteNetwork-Velocity.jar,dist/VoteNetwork-Spigot.jar -DestinationPath dist/VoteNetwork.zip -Force"
fi
echo "==> Creation de la release Gitea ${TAG}"
RELEASE_JSON=$(curl -sf -X POST "${GITEA_HOST}/api/v1/repos/${OWNER}/${REPO}/releases" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${TAG}\",\"name\":\"VoteNetwork ${VERSION}\",\"body\":$(printf '%s' "$NOTES" | python3 -c 'import json,sys;print(json.dumps(sys.stdin.read()))' 2>/dev/null || printf '"%s"' "$NOTES"),\"draft\":false,\"prerelease\":false}")
RELEASE_ID=$(printf '%s' "$RELEASE_JSON" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
if [ -z "$RELEASE_ID" ]; then
echo "Erreur : creation de la release echouee." >&2
echo "$RELEASE_JSON" >&2
exit 1
fi
echo "==> Upload du zip (release id ${RELEASE_ID})"
curl -sf -X POST "${GITEA_HOST}/api/v1/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}/assets?name=VoteNetwork.zip" \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@dist/VoteNetwork.zip" > /dev/null
echo "==> Fait : ${GITEA_HOST}/${OWNER}/${REPO}/releases/tag/${TAG}"
+13 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>fr.votenetwork</groupId>
<artifactId>votenetwork-parent</artifactId>
<version>1.0.0</version>
<version>1.0.1</version>
</parent>
<artifactId>votenetwork-spigot</artifactId>
@@ -42,6 +42,18 @@
<version>2.11.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
@@ -1,9 +1,14 @@
package fr.votenetwork.spigot;
import fr.votenetwork.spigot.gui.VoteSite;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
public class VoteConfig {
@@ -141,6 +146,57 @@ public class VoteConfig {
"&cVous n'avez pas la permission d'utiliser cette commande."));
}
public String getGuiTitle() {
return colorize(cfg().getString("gui.title", "&8VoteNetwork"));
}
public String getGuiProgressItemMaterial() {
return cfg().getString("gui.progress-item.material", "NETHER_STAR");
}
public String getGuiProgressItemName() {
return colorize(cfg().getString("gui.progress-item.name", "&b&lVoteParty"));
}
public List<String> getGuiProgressItemLore() {
return colorizeList(cfg().getStringList("gui.progress-item.lore"));
}
public String getGuiClickMessage() {
return colorize(cfg().getString("gui.click-message", "&a[Vote] Clique sur le lien pour voter :"));
}
/**
* Parses gui.vote-sites (a YAML list of maps: name/material/url/lore) into VoteSite
* objects, one item per configured vote site.
*/
public List<VoteSite> getVoteSites() {
List<VoteSite> sites = new ArrayList<>();
for (Map<?, ?> raw : cfg().getMapList("gui.vote-sites")) {
String name = raw.containsKey("name") ? String.valueOf(raw.get("name")) : "&aVoter";
String materialName = raw.containsKey("material") ? String.valueOf(raw.get("material")) : "PAPER";
String url = raw.containsKey("url") ? String.valueOf(raw.get("url")) : "";
List<String> lore = new ArrayList<>();
Object loreObj = raw.get("lore");
if (loreObj instanceof List<?> loreList) {
for (Object line : loreList) {
lore.add(String.valueOf(line));
}
}
Material material;
try {
material = Material.valueOf(materialName.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
material = Material.PAPER;
}
sites.add(new VoteSite(colorize(name), material, url, colorizeList(lore)));
}
return sites;
}
private String colorize(String input) {
return input == null ? "" : ChatColor.translateAlternateColorCodes('&', input);
}
@@ -5,6 +5,8 @@ import fr.votenetwork.spigot.api.VoteNetworkAPIHolder;
import fr.votenetwork.spigot.api.VoteNetworkAPIImpl;
import fr.votenetwork.spigot.command.ClaimCommand;
import fr.votenetwork.spigot.command.VoteAdminCommand;
import fr.votenetwork.spigot.command.VoteGuiCommand;
import fr.votenetwork.spigot.gui.VotePartyGuiListener;
import fr.votenetwork.spigot.listener.VoteMessageListener;
import fr.votenetwork.spigot.placeholder.VoteNetworkPlaceholders;
import fr.votenetwork.spigot.storage.MySqlPendingVoteStore;
@@ -55,6 +57,8 @@ public class VoteNetworkSpigot extends JavaPlugin {
getCommand("claim").setExecutor(new ClaimCommand(this, voteConfig, pendingVoteStore, voteParty, maintenanceState));
getCommand("vote").setExecutor(new VoteAdminCommand(this, voteConfig, pendingVoteStore, maintenanceState));
getCommand("votegui").setExecutor(new VoteGuiCommand(voteConfig, voteParty));
getServer().getPluginManager().registerEvents(new VotePartyGuiListener(voteConfig), this);
VoteNetworkAPI api = new VoteNetworkAPIImpl(this, voteConfig, pendingVoteStore, voteParty, maintenanceState);
VoteNetworkAPIHolder.set(api);
@@ -0,0 +1,31 @@
package fr.votenetwork.spigot.command;
import fr.votenetwork.spigot.VoteConfig;
import fr.votenetwork.spigot.gui.VotePartyGui;
import fr.votenetwork.spigot.voteparty.VoteParty;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class VoteGuiCommand implements CommandExecutor {
private final VoteConfig config;
private final VoteParty voteParty;
public VoteGuiCommand(VoteConfig config, VoteParty voteParty) {
this.config = config;
this.voteParty = voteParty;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(ChatColor.RED + "Commande reservee aux joueurs.");
return true;
}
VotePartyGui.open(player, config, voteParty);
return true;
}
}
@@ -0,0 +1,72 @@
package fr.votenetwork.spigot.gui;
import fr.votenetwork.spigot.VoteConfig;
import fr.votenetwork.spigot.voteparty.VoteParty;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.List;
/**
* Builds and opens the VoteParty progress GUI: a progress item (slot 4) plus one item per
* configured vote site (starting at slot 9), each opening its URL when clicked.
*/
public final class VotePartyGui {
public static final int PROGRESS_SLOT = 4;
public static final int SITES_START_SLOT = 9;
private VotePartyGui() {
}
public static void open(Player player, VoteConfig config, VoteParty voteParty) {
List<VoteSite> sites = config.getVoteSites();
int size = ((SITES_START_SLOT + sites.size() + 8) / 9) * 9;
size = Math.max(size, 18);
VotePartyGuiHolder holder = new VotePartyGuiHolder();
Inventory inventory = Bukkit.createInventory(holder, size, config.getGuiTitle());
holder.setInventory(inventory);
int required = config.getVotePartyVotesRequired();
int current = Math.min(voteParty.getCurrentVotes(), required);
ItemStack progressItem = new ItemStack(materialOrFallback(config.getGuiProgressItemMaterial()));
ItemMeta progressMeta = progressItem.getItemMeta();
if (progressMeta != null) {
progressMeta.setDisplayName(config.getGuiProgressItemName());
progressMeta.setLore(config.getGuiProgressItemLore().stream()
.map(line -> line.replace("%current%", String.valueOf(current))
.replace("%required%", String.valueOf(required)))
.toList());
progressItem.setItemMeta(progressMeta);
}
inventory.setItem(PROGRESS_SLOT, progressItem);
int slot = SITES_START_SLOT;
for (VoteSite site : sites) {
ItemStack item = new ItemStack(site.getMaterial());
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(site.getName());
meta.setLore(site.getLore());
item.setItemMeta(meta);
}
inventory.setItem(slot, item);
slot++;
}
player.openInventory(inventory);
}
private static org.bukkit.Material materialOrFallback(String name) {
try {
return org.bukkit.Material.valueOf(name.toUpperCase(java.util.Locale.ROOT));
} catch (IllegalArgumentException e) {
return org.bukkit.Material.NETHER_STAR;
}
}
}
@@ -0,0 +1,23 @@
package fr.votenetwork.spigot.gui;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
/**
* Marker holder used to recognize our own GUI inventory in InventoryClickEvent, so the
* listener never touches clicks happening in unrelated inventories (chests, other plugins'
* GUIs, etc.).
*/
public class VotePartyGuiHolder implements InventoryHolder {
private Inventory inventory;
@Override
public Inventory getInventory() {
return inventory;
}
public void setInventory(Inventory inventory) {
this.inventory = inventory;
}
}
@@ -0,0 +1,53 @@
package fr.votenetwork.spigot.gui;
import fr.votenetwork.spigot.VoteConfig;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import java.util.List;
/**
* Bukkit cannot open a browser directly from an inventory click, so a click on a vote-site
* item closes the GUI and sends a clickable chat message (ClickEvent OPEN_URL) instead.
*/
public class VotePartyGuiListener implements Listener {
private final VoteConfig config;
public VotePartyGuiListener(VoteConfig config) {
this.config = config;
}
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (!(event.getInventory().getHolder() instanceof VotePartyGuiHolder)) {
return;
}
event.setCancelled(true);
int slot = event.getRawSlot();
if (slot < VotePartyGui.SITES_START_SLOT || !(event.getWhoClicked() instanceof Player player)) {
return;
}
List<VoteSite> sites = config.getVoteSites();
int index = slot - VotePartyGui.SITES_START_SLOT;
if (index < 0 || index >= sites.size()) {
return;
}
VoteSite site = sites.get(index);
if (site.getUrl() == null || site.getUrl().isEmpty()) {
return;
}
player.closeInventory();
TextComponent message = new TextComponent(config.getGuiClickMessage() + " " + site.getUrl());
message.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, site.getUrl()));
player.spigot().sendMessage(message);
}
}
@@ -0,0 +1,40 @@
package fr.votenetwork.spigot.gui;
import org.bukkit.Material;
import java.util.List;
/**
* A single vote-site entry configured in config.yml (gui.vote-sites): the display item and
* the URL opened (via a clickable chat message) when the player clicks it in the GUI.
*/
public class VoteSite {
private final String name;
private final Material material;
private final String url;
private final List<String> lore;
public VoteSite(String name, Material material, String url, List<String> lore) {
this.name = name;
this.material = material;
this.url = url;
this.lore = lore;
}
public String getName() {
return name;
}
public Material getMaterial() {
return material;
}
public String getUrl() {
return url;
}
public List<String> getLore() {
return lore;
}
}
+24
View File
@@ -47,6 +47,30 @@ voteparty:
progress-message: "&b[Vote] &f%current%&7/&f%required% &fvotes avant le prochain VoteParty !"
triggered-message: "&a[Vote] &fVoteParty declenche ! Profitez des recompenses !"
# GUI /votegui : affiche la progression du VoteParty et des items cliquables vers les sites
# de vote (ouvre le lien via un message chat cliquable, Bukkit ne peut pas ouvrir un navigateur
# directement depuis un clic d'inventaire).
gui:
title: "&8VoteNetwork"
progress-item:
material: NETHER_STAR
name: "&b&lVoteParty"
lore:
- "&7Progression actuelle :"
- "&f%current%&7/&f%required% &7votes"
click-message: "&a[Vote] Clique sur le lien pour voter :"
vote-sites:
- name: "&aVoter sur le site 1"
material: PAPER
url: "https://example.com/vote/site1"
lore:
- "&7Clique pour voter !"
- name: "&aVoter sur le site 2"
material: BOOK
url: "https://example.com/vote/site2"
lore:
- "&7Clique pour voter !"
messages:
direct-vote: "&a[Vote] &fMerci pour ton vote !"
claim-success: "&a[Vote] &fVous avez recupere &e%amount% &fvote(s) en attente !"
+3
View File
@@ -14,6 +14,9 @@ commands:
description: Administration du systeme de vote (maintenance, test du stockage).
usage: /vote <stop|start|testdb>
permission: votenetwork.admin
votegui:
description: Ouvre le GUI de progression du VoteParty avec les liens de vote.
usage: /votegui
permissions:
votenetwork.admin:
@@ -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");
}
}
@@ -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<Boolean> 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);
}
}
+7 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>fr.votenetwork</groupId>
<artifactId>votenetwork-parent</artifactId>
<version>1.0.0</version>
<version>1.0.1</version>
</parent>
<artifactId>votenetwork-velocity</artifactId>
@@ -36,6 +36,12 @@
<version>2.10.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
@@ -14,7 +14,7 @@ import java.nio.file.Path;
@Plugin(
id = "votenetwork",
name = "VoteNetwork",
version = "1.0.0",
version = "1.0.1",
description = "Systeme de vote reseau Velocity <-> Spigot",
authors = {"Sar_Tron"}
)
@@ -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);
}
}
@@ -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"));
}
}