Skip to content

Commit a67b76c

Browse files
committed
test: add unit tests for core disenchant logic
1 parent 3a5b8ba commit a67b76c

6 files changed

Lines changed: 1659 additions & 0 deletions

File tree

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
package com.example.disenchanter.core;
2+
3+
import com.example.disenchanter.config.ConfigManager;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Nested;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.ExtendWith;
9+
import org.mockito.Mock;
10+
import org.mockito.junit.jupiter.MockitoExtension;
11+
12+
import java.util.List;
13+
import java.util.Map;
14+
15+
import static org.junit.jupiter.api.Assertions.*;
16+
import static org.mockito.Mockito.doReturn;
17+
import static org.mockito.Mockito.lenient;
18+
19+
/**
20+
* Unit tests for {@link CostCalculator}.
21+
* <p>
22+
* Tests individual/bulk cost calculation, curse multipliers,
23+
* bypass permission, and item cost parsing.
24+
* Uses Mockito to stub {@link ConfigManager} — no Bukkit runtime required.
25+
*/
26+
@ExtendWith(MockitoExtension.class)
27+
@DisplayName("CostCalculator")
28+
class CostCalculatorTest {
29+
30+
@Mock
31+
private ConfigManager configManager;
32+
33+
private CostCalculator calculator;
34+
35+
@BeforeEach
36+
void setUp() {
37+
calculator = new CostCalculator(configManager);
38+
39+
// Default config values (matching spec defaults)
40+
lenient().doReturn(3).when(configManager).getIndividualExpLevel();
41+
lenient().doReturn(100.0).when(configManager).getIndividualMoney();
42+
lenient().doReturn(List.of()).when(configManager).getIndividualItemCosts();
43+
lenient().doReturn(10).when(configManager).getBulkExpLevel();
44+
lenient().doReturn(500.0).when(configManager).getBulkMoney();
45+
lenient().doReturn(List.of()).when(configManager).getBulkItemCosts();
46+
lenient().doReturn(2.0).when(configManager).getCursesExtraCostMultiplier();
47+
}
48+
49+
// ── Helper for item costs stubbing ────────────────────────
50+
51+
@SuppressWarnings("unchecked")
52+
private void stubItemCosts(List<?> rawItems) {
53+
lenient().doReturn(rawItems).when(configManager).getIndividualItemCosts();
54+
}
55+
56+
@SuppressWarnings("unchecked")
57+
private void stubBulkItemCosts(List<?> rawItems) {
58+
lenient().doReturn(rawItems).when(configManager).getBulkItemCosts();
59+
}
60+
61+
// ── Individual cost ───────────────────────────────────────
62+
63+
@Nested
64+
@DisplayName("calculateIndividualCost")
65+
class IndividualCost {
66+
67+
@Test
68+
@DisplayName("returns FREE when bypass is true")
69+
void bypassReturnsFree() {
70+
CostCalculator.CostResult result = calculator.calculateIndividualCost(true, false);
71+
assertTrue(result.isFree());
72+
assertEquals(0, result.expLevels());
73+
assertEquals(0.0, result.money());
74+
assertTrue(result.itemCosts().isEmpty());
75+
}
76+
77+
@Test
78+
@DisplayName("returns normal exp cost from config")
79+
void normalExpCost() {
80+
lenient().doReturn(5).when(configManager).getIndividualExpLevel();
81+
lenient().doReturn(0.0).when(configManager).getIndividualMoney();
82+
83+
CostCalculator.CostResult result = calculator.calculateIndividualCost(false, false);
84+
85+
assertEquals(5, result.expLevels());
86+
assertEquals(0.0, result.money());
87+
}
88+
89+
@Test
90+
@DisplayName("returns money cost from config")
91+
void normalMoneyCost() {
92+
lenient().doReturn(250.0).when(configManager).getIndividualMoney();
93+
94+
CostCalculator.CostResult result = calculator.calculateIndividualCost(false, false);
95+
96+
assertEquals(3, result.expLevels());
97+
assertEquals(250.0, result.money());
98+
}
99+
100+
@Test
101+
@DisplayName("parses item costs from config (Map format)")
102+
void itemCostsFromConfig() {
103+
stubItemCosts(List.of(Map.of("material", "DIAMOND", "amount", 3)));
104+
lenient().doReturn(0.0).when(configManager).getIndividualMoney();
105+
106+
CostCalculator.CostResult result = calculator.calculateIndividualCost(false, false);
107+
108+
assertEquals(1, result.itemCosts().size());
109+
assertEquals("DIAMOND", result.itemCosts().get(0).material());
110+
assertEquals(3, result.itemCosts().get(0).amount());
111+
}
112+
113+
@Test
114+
@DisplayName("handles empty item costs list")
115+
void emptyItemCosts() {
116+
stubItemCosts(List.of());
117+
118+
CostCalculator.CostResult result = calculator.calculateIndividualCost(false, false);
119+
120+
assertTrue(result.itemCosts().isEmpty());
121+
}
122+
123+
@Test
124+
@DisplayName("handles null item costs list")
125+
void nullItemCosts() {
126+
stubItemCosts(null);
127+
128+
CostCalculator.CostResult result = calculator.calculateIndividualCost(false, false);
129+
130+
assertTrue(result.itemCosts().isEmpty());
131+
}
132+
}
133+
134+
// ── Bulk cost ─────────────────────────────────────────────
135+
136+
@Nested
137+
@DisplayName("calculateBulkCost")
138+
class BulkCost {
139+
140+
@Test
141+
@DisplayName("returns FREE when bypass is true")
142+
void bypassReturnsFree() {
143+
CostCalculator.CostResult result = calculator.calculateBulkCost(true, 0);
144+
assertTrue(result.isFree());
145+
}
146+
147+
@Test
148+
@DisplayName("returns bulk exp cost from config")
149+
void bulkExpCost() {
150+
lenient().doReturn(15).when(configManager).getBulkExpLevel();
151+
lenient().doReturn(0.0).when(configManager).getBulkMoney();
152+
153+
CostCalculator.CostResult result = calculator.calculateBulkCost(false, 0);
154+
155+
assertEquals(15, result.expLevels());
156+
}
157+
158+
@Test
159+
@DisplayName("returns bulk money cost from config")
160+
void bulkMoneyCost() {
161+
lenient().doReturn(1000.0).when(configManager).getBulkMoney();
162+
163+
CostCalculator.CostResult result = calculator.calculateBulkCost(false, 0);
164+
165+
assertEquals(10, result.expLevels());
166+
assertEquals(1000.0, result.money());
167+
}
168+
169+
@Test
170+
@DisplayName("bulk item costs from config")
171+
void bulkItemCosts() {
172+
stubBulkItemCosts(List.of(Map.of("material", "LAPIS_LAZULI", "amount", 5)));
173+
lenient().doReturn(0.0).when(configManager).getBulkMoney();
174+
175+
CostCalculator.CostResult result = calculator.calculateBulkCost(false, 0);
176+
177+
assertEquals(1, result.itemCosts().size());
178+
assertEquals("LAPIS_LAZULI", result.itemCosts().get(0).material());
179+
assertEquals(5, result.itemCosts().get(0).amount());
180+
}
181+
}
182+
183+
// ── Curse multiplier ──────────────────────────────────────
184+
185+
@Nested
186+
@DisplayName("curse multiplier")
187+
class CurseMultiplier {
188+
189+
@Test
190+
@DisplayName("individual cost applies curse multiplier when isCurse=true")
191+
void individualCurseAppliesMultiplier() {
192+
lenient().doReturn(3.0).when(configManager).getCursesExtraCostMultiplier();
193+
194+
CostCalculator.CostResult result = calculator.calculateIndividualCost(false, true);
195+
196+
// 3 * 3.0 = 9 exp levels
197+
assertEquals(9, result.expLevels());
198+
// 100.0 * 3.0 = 300.0 money
199+
assertEquals(300.0, result.money(), 0.001);
200+
}
201+
202+
@Test
203+
@DisplayName("individual cost does NOT apply multiplier when isCurse=false")
204+
void individualNoCurseNoMultiplier() {
205+
CostCalculator.CostResult result = calculator.calculateIndividualCost(false, false);
206+
207+
assertEquals(3, result.expLevels());
208+
assertEquals(100.0, result.money(), 0.001);
209+
}
210+
211+
@Test
212+
@DisplayName("bulk cost applies curse multiplier when curseCount > 0")
213+
void bulkCurseAppliesMultiplier() {
214+
lenient().doReturn(2.0).when(configManager).getCursesExtraCostMultiplier();
215+
lenient().doReturn(10).when(configManager).getBulkExpLevel();
216+
lenient().doReturn(500.0).when(configManager).getBulkMoney();
217+
218+
CostCalculator.CostResult result = calculator.calculateBulkCost(false, 2);
219+
220+
assertEquals(20, result.expLevels());
221+
assertEquals(1000.0, result.money(), 0.001);
222+
}
223+
224+
@Test
225+
@DisplayName("bulk cost does NOT apply multiplier when curseCount == 0")
226+
void bulkNoCurseNoMultiplier() {
227+
CostCalculator.CostResult result = calculator.calculateBulkCost(false, 0);
228+
229+
assertEquals(10, result.expLevels());
230+
assertEquals(500.0, result.money(), 0.001);
231+
}
232+
233+
@Test
234+
@DisplayName("bulk cost with curseCount=0 uses multiplier=1.0")
235+
void bulkZeroCurseCount() {
236+
lenient().doReturn(5.0).when(configManager).getCursesExtraCostMultiplier();
237+
lenient().doReturn(10).when(configManager).getBulkExpLevel();
238+
239+
CostCalculator.CostResult result = calculator.calculateBulkCost(false, 0);
240+
241+
// curseCount==0 → multiplier=1.0, NOT 5.0
242+
assertEquals(10, result.expLevels());
243+
}
244+
}
245+
246+
// ── Item cost multiplier for curses ───────────────────────
247+
248+
@Nested
249+
@DisplayName("item cost with curse multiplier")
250+
class ItemCostCurseMultiplier {
251+
252+
@Test
253+
@DisplayName("item amount is multiplied by curse multiplier and floored at 1")
254+
void itemAmountMultipliedForCurse() {
255+
lenient().doReturn(2.0).when(configManager).getCursesExtraCostMultiplier();
256+
lenient().doReturn(0.0).when(configManager).getIndividualMoney();
257+
stubItemCosts(List.of(Map.of("material", "DIAMOND", "amount", 3)));
258+
259+
CostCalculator.CostResult result = calculator.calculateIndividualCost(false, true);
260+
261+
assertEquals(1, result.itemCosts().size());
262+
// 3 * 2.0 = 6
263+
assertEquals(6, result.itemCosts().get(0).amount());
264+
}
265+
266+
@Test
267+
@DisplayName("item amount minimum is 1 even after rounding")
268+
void itemAmountMinimumOne() {
269+
lenient().doReturn(0.1).when(configManager).getCursesExtraCostMultiplier();
270+
lenient().doReturn(0.0).when(configManager).getIndividualMoney();
271+
stubItemCosts(List.of(Map.of("material", "DIAMOND", "amount", 3)));
272+
273+
CostCalculator.CostResult result = calculator.calculateIndividualCost(false, true);
274+
275+
// 3 * 0.1 = 0.3, Math.round = 0, Math.max(1, 0) = 1
276+
assertEquals(1, result.itemCosts().get(0).amount());
277+
}
278+
}
279+
280+
// ── CostResult record ─────────────────────────────────────
281+
282+
@Nested
283+
@DisplayName("CostResult record")
284+
class CostResultTests {
285+
286+
@Test
287+
@DisplayName("FREE is zero cost")
288+
void freeIsZero() {
289+
assertTrue(CostCalculator.CostResult.FREE.isFree());
290+
assertEquals(0, CostCalculator.CostResult.FREE.expLevels());
291+
assertEquals(0.0, CostCalculator.CostResult.FREE.money());
292+
assertTrue(CostCalculator.CostResult.FREE.itemCosts().isEmpty());
293+
}
294+
295+
@Test
296+
@DisplayName("isFree returns true only when all costs are zero")
297+
void isFreeLogic() {
298+
assertTrue(new CostCalculator.CostResult(0, 0.0, List.of()).isFree());
299+
assertFalse(new CostCalculator.CostResult(1, 0.0, List.of()).isFree());
300+
assertFalse(new CostCalculator.CostResult(0, 1.0, List.of()).isFree());
301+
assertFalse(new CostCalculator.CostResult(0, 0.0,
302+
List.of(new CostCalculator.ItemCostEntry("DIAMOND", 1))).isFree());
303+
}
304+
305+
@Test
306+
@DisplayName("toString produces readable output")
307+
void toStringReadable() {
308+
CostCalculator.CostResult result = new CostCalculator.CostResult(5, 100.0, List.of());
309+
String str = result.toString();
310+
assertTrue(str.contains("exp:5"));
311+
assertTrue(str.contains("money:100.0"));
312+
}
313+
}
314+
315+
// ── ItemCostEntry record ──────────────────────────────────
316+
317+
@Nested
318+
@DisplayName("ItemCostEntry record")
319+
class ItemCostEntryTests {
320+
321+
@Test
322+
@DisplayName("toString produces readable output")
323+
void toStringReadable() {
324+
CostCalculator.ItemCostEntry entry = new CostCalculator.ItemCostEntry("DIAMOND", 3);
325+
assertEquals("DIAMOND x3", entry.toString());
326+
}
327+
}
328+
329+
// ── Vault disabled (money = 0) ────────────────────────────
330+
331+
@Nested
332+
@DisplayName("Vault disabled handling")
333+
class VaultDisabled {
334+
335+
@Test
336+
@DisplayName("money cost from config is used as-is (MoneyCost handles Vault availability)")
337+
void moneyCostFromConfigPassedThrough() {
338+
// CostCalculator just reads and passes through the config value.
339+
// MoneyCost itself checks Vault availability and treats money==0
340+
// or economy==null as satisfied.
341+
// This test verifies CostCalculator doesn't zero-out money on its own.
342+
lenient().doReturn(100.0).when(configManager).getIndividualMoney();
343+
344+
CostCalculator.CostResult result = calculator.calculateIndividualCost(false, false);
345+
346+
// CostCalculator faithfully returns whatever config says
347+
assertEquals(100.0, result.money(), 0.001);
348+
}
349+
}
350+
}

0 commit comments

Comments
 (0)