diff --git a/pom.xml b/pom.xml
index 26f377a..3a01827 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,6 +52,31 @@
mysql-connector-j
8.4.0
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.2
+ test
+
+
+ net.jqwik
+ jqwik
+ 1.8.2
+ test
+
+
+ com.github.seeseemelk
+ MockBukkit-v1.20
+ 3.14.0
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.11.0
+ test
+
@@ -80,6 +105,14 @@
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.1.2
+
+ false
+
+
diff --git a/src/test/java/com/yourorg/servershop/dynamic/PricingPropertiesTest.java b/src/test/java/com/yourorg/servershop/dynamic/PricingPropertiesTest.java
new file mode 100644
index 0000000..5d48d61
--- /dev/null
+++ b/src/test/java/com/yourorg/servershop/dynamic/PricingPropertiesTest.java
@@ -0,0 +1,107 @@
+package com.yourorg.servershop.dynamic;
+
+import be.seeseemelk.mockbukkit.MockBukkit;
+import be.seeseemelk.mockbukkit.ServerMock;
+import net.jqwik.api.*;
+import org.bukkit.Material;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.ServicePriority;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Assertions;
+
+import com.yourorg.servershop.ServerShopPlugin;
+import com.yourorg.servershop.shop.ItemEntry;
+
+import net.milkbowl.vault.economy.Economy;
+import net.milkbowl.vault.economy.EconomyResponse;
+
+import org.mockito.Mockito;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyDouble;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+
+public class PricingPropertiesTest {
+ static ServerMock server;
+ static ServerShopPlugin plugin;
+
+ @BeforeAll
+ static void init() {
+ server = MockBukkit.mock();
+ Plugin vault = MockBukkit.createMockPlugin("Vault");
+ Economy econ = Mockito.mock(Economy.class, Mockito.RETURNS_DEEP_STUBS);
+ Mockito.when(econ.withdrawPlayer(any(OfflinePlayer.class), anyDouble()))
+ .thenReturn(new EconomyResponse(0, 0, EconomyResponse.ResponseType.SUCCESS, ""));
+ Mockito.when(econ.depositPlayer(any(OfflinePlayer.class), anyDouble()))
+ .thenReturn(new EconomyResponse(0, 0, EconomyResponse.ResponseType.SUCCESS, ""));
+ Mockito.when(econ.getBalance(any(OfflinePlayer.class))).thenReturn(1_000_000.0);
+ server.getServicesManager().register(Economy.class, econ, vault, ServicePriority.Normal);
+ plugin = MockBukkit.load(ServerShopPlugin.class);
+ }
+
+ @AfterAll
+ static void shutdown() {
+ MockBukkit.unmock();
+ }
+
+ @Provide
+ Arbitrary materials() {
+ return Arbitraries.of(plugin.catalog().allMaterials());
+ }
+
+ @Provide
+ Arbitrary counts() {
+ return Arbitraries.integers().between(1, 10_000);
+ }
+
+ @Property
+ void priceWithinBounds(@ForAll("materials") Material mat) {
+ ItemEntry entry = plugin.catalog().get(mat).orElseThrow();
+ DynamicPricingManager mgr = new DynamicPricingManager(plugin);
+ double buy = mgr.buyPrice(mat, entry.buyPrice());
+ double sell = mgr.sellPrice(mat, entry.sellPrice());
+
+ double minBuy = Math.max(0.01, entry.buyPrice() * plugin.getConfig().getDouble("priceModel.minFactor"));
+ double maxBuy = Math.max(minBuy, entry.buyPrice() * plugin.getConfig().getDouble("priceModel.maxFactor"));
+ Assertions.assertTrue(buy >= minBuy && buy <= maxBuy);
+
+ double minSell = Math.max(0.01, entry.sellPrice() * plugin.getConfig().getDouble("priceModel.minFactor"));
+ double maxSell = Math.max(minSell, entry.sellPrice() * plugin.getConfig().getDouble("priceModel.maxFactor"));
+ Assertions.assertTrue(sell >= minSell && sell <= maxSell);
+ }
+
+ @Property
+ void spreadRespected(@ForAll("materials") Material mat) {
+ ItemEntry entry = plugin.catalog().get(mat).orElseThrow();
+ DynamicPricingManager mgr = new DynamicPricingManager(plugin);
+ double buy = mgr.buyPrice(mat, entry.buyPrice());
+ double sell = mgr.sellPrice(mat, entry.sellPrice());
+ Assertions.assertTrue(buy >= sell);
+ }
+
+ @Property
+ void adjustmentsCapped(@ForAll("materials") Material mat,
+ @ForAll("counts") int buyQty,
+ @ForAll("counts") int sellQty) throws Exception {
+ DynamicPricingManager mgr = new DynamicPricingManager(plugin);
+ Map map = stateMap(mgr);
+ mgr.adjustOnBuy(mat, buyQty);
+ double maxMult = plugin.getConfig().getDouble("dynamicPricing.maxMultiplier");
+ double minMult = plugin.getConfig().getDouble("dynamicPricing.minMultiplier");
+ double afterBuy = map.get(mat).multiplier;
+ Assertions.assertTrue(afterBuy <= maxMult);
+ mgr.adjustOnSell(mat, sellQty);
+ double afterSell = map.get(mat).multiplier;
+ Assertions.assertTrue(afterSell >= minMult && afterSell <= maxMult);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map stateMap(DynamicPricingManager mgr) throws Exception {
+ Field f = DynamicPricingManager.class.getDeclaredField("map");
+ f.setAccessible(true);
+ return (Map) f.get(mgr);
+ }
+}