fix: resolve 11 P1 bugs from audit (leaderboard, SQL, coop, jump, lifecycle, schematics, config)
- leaderboard: parse M:SS.mmm/H:MM:SS(.mmm), switch TIME sort to long (п.25) - sql: single Hikari pool, non-blocking connect, adaptive init order, settings JSON round-trip (п.27-30) - coop: remove unregister recursion via session-only leave + reentrancy guard (п.24) - jump: perpendicular jitter for diagonal heading, valid safety-fallback on retry exhaustion (п.32-33) - lifecycle: fall threshold relative to course, lead via generated-blocks counter, one-time rewards once (п.35-37) - schematics: catch RuntimeExceptions, validate size, 10MB file limit (п.31) - config merge: preserve block-sequence lists and ignored sections, fix path reversal (п.26) - tests: +31 (score, jump, schematics, config merge) = 72 total
This commit is contained in:
parent
c0ca488b9d
commit
fea15e61c8
27 changed files with 1012 additions and 167 deletions
|
|
@ -0,0 +1,249 @@
|
|||
package dev.loki.loparkour.config.core.merge;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Unit tests for the template/user config merge. Uses 2-space indentation,
|
||||
* matching the shipped config templates.
|
||||
*/
|
||||
class ConfigMergeProcessorTest {
|
||||
|
||||
private static final String TEMPLATE = """
|
||||
debug: false
|
||||
options:
|
||||
leads:
|
||||
amount:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
- 8
|
||||
- 9
|
||||
- 10
|
||||
|
||||
time:
|
||||
format: 24
|
||||
|
||||
focus-mode:
|
||||
whitelist:
|
||||
- 'ip'
|
||||
- 'msg'
|
||||
|
||||
styles:
|
||||
list:
|
||||
red: [red_wool, red_stained_glass]
|
||||
""";
|
||||
|
||||
private final ConfigMergeProcessor processor = new ConfigMergeProcessor();
|
||||
|
||||
@Test
|
||||
void customizedBlockListSurvivesMerge() {
|
||||
final List<String> user = List.of(
|
||||
"debug: true",
|
||||
"options:",
|
||||
" leads:",
|
||||
" amount:",
|
||||
" - 1",
|
||||
" - 2",
|
||||
" - 3",
|
||||
"",
|
||||
" time:",
|
||||
" format: 12");
|
||||
|
||||
final List<String> merged = processor.merge(lines(TEMPLATE), user, null);
|
||||
|
||||
assertTrue(merged.contains(" - 1"));
|
||||
assertTrue(merged.contains(" - 2"));
|
||||
assertTrue(merged.contains(" - 3"));
|
||||
assertFalse(merged.contains(" - 4"));
|
||||
assertTrue(merged.contains("debug: true"));
|
||||
assertTrue(merged.contains(" format: 12"));
|
||||
assertTrue(merged.contains(" - 'ip'"));
|
||||
assertTrue(merged.contains(" red: [red_wool, red_stained_glass]"));
|
||||
assertValidYaml(merged);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unchangedBlockListStaysIdentical() {
|
||||
final List<String> template = lines(TEMPLATE);
|
||||
assertEquals(template, processor.merge(template, template, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void newTemplateKeysAreAdded() {
|
||||
final List<String> template = List.of(
|
||||
"old-key: 'old'",
|
||||
"new-key: 'new'",
|
||||
"new-section:",
|
||||
" enabled: true");
|
||||
final List<String> user = List.of("old-key: 'custom'");
|
||||
|
||||
final List<String> merged = processor.merge(template, user, null);
|
||||
|
||||
assertTrue(merged.contains("old-key: 'custom'"));
|
||||
assertTrue(merged.contains("new-key: 'new'"));
|
||||
assertTrue(merged.contains("new-section:"));
|
||||
assertTrue(merged.contains(" enabled: true"));
|
||||
assertValidYaml(merged);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoredSectionUserCustomizedIsPreserved() {
|
||||
final List<String> user = List.of(
|
||||
"styles:",
|
||||
" list:",
|
||||
" red: [custom_red_wool]",
|
||||
" extra: [extra_wool]");
|
||||
|
||||
final List<String> merged = processor.merge(lines(TEMPLATE), user, List.of("styles"));
|
||||
|
||||
assertTrue(merged.contains(" red: [custom_red_wool]"));
|
||||
assertTrue(merged.contains(" extra: [extra_wool]"));
|
||||
assertFalse(merged.contains(" red: [red_wool, red_stained_glass]"));
|
||||
assertValidYaml(merged);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoredSectionMissingUsesTemplate() {
|
||||
final List<String> user = List.of("debug: false");
|
||||
|
||||
final List<String> merged = processor.merge(lines(TEMPLATE), user, List.of("styles"));
|
||||
|
||||
assertTrue(merged.contains("styles:"));
|
||||
assertTrue(merged.contains(" list:"));
|
||||
assertTrue(merged.contains(" red: [red_wool, red_stained_glass]"));
|
||||
assertValidYaml(merged);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoredSectionBlockListIsPreserved() {
|
||||
final List<String> template = List.of(
|
||||
"focus-mode:",
|
||||
" enabled: false",
|
||||
" whitelist:",
|
||||
" - 'ip'",
|
||||
" - 'msg'");
|
||||
final List<String> user = List.of(
|
||||
"focus-mode:",
|
||||
" enabled: true",
|
||||
" whitelist:",
|
||||
" - 'custom'");
|
||||
|
||||
final List<String> merged = processor.merge(template, user, List.of("focus-mode"));
|
||||
|
||||
assertTrue(merged.contains(" enabled: true"));
|
||||
assertTrue(merged.contains(" - 'custom'"));
|
||||
assertFalse(merged.contains("- 'ip'"));
|
||||
assertValidYaml(merged);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fileBasedMergeAppliesBlockList(@TempDir Path tempDir) throws IOException {
|
||||
final Path templateFile = tempDir.resolve("template.yml");
|
||||
final Path existingFile = tempDir.resolve("existing.yml");
|
||||
Files.write(templateFile, lines(TEMPLATE));
|
||||
Files.write(existingFile, List.of(
|
||||
"debug: true",
|
||||
"options:",
|
||||
" leads:",
|
||||
" amount:",
|
||||
" - 1",
|
||||
" - 2",
|
||||
" - 3"));
|
||||
|
||||
try (InputStream is = Files.newInputStream(templateFile)) {
|
||||
final List<String> merged = processor.merge(is, existingFile.toFile(), null);
|
||||
assertTrue(merged.contains(" - 1"));
|
||||
assertTrue(merged.contains(" - 3"));
|
||||
assertFalse(merged.contains(" - 4"));
|
||||
assertTrue(merged.contains("debug: true"));
|
||||
assertValidYaml(merged);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoredSectionWithSiblingScalarsIsPreserved() {
|
||||
final List<String> template = List.of(
|
||||
"difficulty:",
|
||||
" d0e227d2: 0.25",
|
||||
" dbef835e: 0.25",
|
||||
" 27f41406: 1.0");
|
||||
final List<String> user = List.of(
|
||||
"difficulty:",
|
||||
" d0e227d2: 0.5",
|
||||
" dbef835e: 0.25",
|
||||
" abc12345: 0.75");
|
||||
|
||||
final List<String> merged = processor.merge(template, user, List.of("difficulty"));
|
||||
|
||||
assertTrue(merged.contains(" d0e227d2: 0.5"));
|
||||
assertTrue(merged.contains(" dbef835e: 0.25"));
|
||||
assertTrue(merged.contains(" abc12345: 0.75"));
|
||||
assertFalse(merged.contains("27f41406: 1.0"));
|
||||
assertValidYaml(merged);
|
||||
}
|
||||
|
||||
@Test
|
||||
void siblingScalarsAllSubstitute() {
|
||||
final List<String> template = List.of(
|
||||
"world:",
|
||||
" name: 'loparkour'",
|
||||
" fall-back: 'world'",
|
||||
" delete-on-reload: true");
|
||||
final List<String> user = List.of(
|
||||
"world:",
|
||||
" name: 'custom'",
|
||||
" fall-back: 'custom-world'");
|
||||
|
||||
final List<String> merged = processor.merge(template, user, null);
|
||||
|
||||
assertTrue(merged.contains(" name: 'custom'"));
|
||||
assertTrue(merged.contains(" fall-back: 'custom-world'"));
|
||||
assertTrue(merged.contains(" delete-on-reload: true"));
|
||||
assertValidYaml(merged);
|
||||
}
|
||||
|
||||
@Test
|
||||
void freshInstallMergesIdentically() throws IOException {
|
||||
assertFreshInstallIdentity("config.yml", List.of("styles"));
|
||||
assertFreshInstallIdentity("generation.yml", null);
|
||||
assertFreshInstallIdentity("rewards-v2.yml", List.of("score-rewards", "interval-rewards", "one-time-rewards"));
|
||||
assertFreshInstallIdentity("schematics/schematics.yml", List.of("difficulty"));
|
||||
}
|
||||
|
||||
private static void assertFreshInstallIdentity(String resource, List<String> ignored) throws IOException {
|
||||
try (InputStream is = ConfigMergeProcessorTest.class.getResourceAsStream("/" + resource)) {
|
||||
assertNotNull(is, "missing resource " + resource);
|
||||
final List<String> template = new BufferedReader(new InputStreamReader(is)).lines().toList();
|
||||
assertEquals(template, new ConfigMergeProcessor().merge(template, template, ignored),
|
||||
"fresh-install merge must be identity for " + resource);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> lines(String content) {
|
||||
return content.lines().toList();
|
||||
}
|
||||
|
||||
private static void assertValidYaml(List<String> merged) {
|
||||
final Object parsed = new Yaml().load(String.join("\n", merged));
|
||||
assertNotNull(parsed);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package dev.loki.loparkour.generator.jump.calculation;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Unit tests for the offset math and the safety-fallback logic of JumpCalculator.
|
||||
*/
|
||||
class JumpCalculatorTest {
|
||||
|
||||
private static final double DELTA = 1.0E-6;
|
||||
private static final double DIAGONAL_UNIT = 0.7071067811865476;
|
||||
private static final double DEFAULT_SD = 1;
|
||||
|
||||
@Mock
|
||||
private World world;
|
||||
|
||||
private Location current;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
current = new Location(world, 0, 100, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void diagonalHeadingOffsetStaysOnPerpendicularBand() {
|
||||
final Vector heading = new Vector(DIAGONAL_UNIT, 0, DIAGONAL_UNIT);
|
||||
final Vector perpendicular = new Vector(-heading.getZ(), 0, heading.getX());
|
||||
final Vector offset = JumpCalculator.offsetFor(heading, 3, 1, 2);
|
||||
|
||||
assertEquals(2, dot(offset, perpendicular), DELTA);
|
||||
assertEquals(3, dot(offset, heading), DELTA);
|
||||
assertEquals(1, offset.getY(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
void xAxisHeadingAppliesJitterOnZAxis() {
|
||||
final Vector offset = JumpCalculator.offsetFor(new Vector(1, 0, 0), 4, 0, -2);
|
||||
|
||||
assertEquals(4, offset.getX(), DELTA);
|
||||
assertEquals(0, offset.getY(), DELTA);
|
||||
assertEquals(-2, offset.getZ(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
void zAxisHeadingAppliesJitterOnXAxis() {
|
||||
final Vector offset = JumpCalculator.offsetFor(new Vector(0, 0, 1), 2, -1, 3);
|
||||
|
||||
assertEquals(-3, offset.getX(), DELTA);
|
||||
assertEquals(-1, offset.getY(), DELTA);
|
||||
assertEquals(2, offset.getZ(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
void safetyFallbackReturnsValidStepForUnitHeading() {
|
||||
final JumpValidator validator = new JumpValidator();
|
||||
final Location fallback = JumpCalculator.safetyFallback(current, new Vector(1, 0, 0), DEFAULT_SD, new Random(42), validator);
|
||||
|
||||
assertNotEquals(current, fallback);
|
||||
assertTrue(validator.canJump(current, fallback));
|
||||
}
|
||||
|
||||
@Test
|
||||
void safetyFallbackReturnsCurrentWhenStraightStepImpossible() {
|
||||
final JumpValidator validator = new JumpValidator();
|
||||
final Location fallback = JumpCalculator.safetyFallback(current, new Vector(100, 0, 0), DEFAULT_SD, new Random(1), validator);
|
||||
|
||||
assertEquals(current, fallback);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateAndRetryJumpNeverReturnsUnreachableBlock() {
|
||||
final JumpValidator validator = new JumpValidator();
|
||||
final Vector heading = new Vector(0, 0, 1);
|
||||
|
||||
for (int distance = 1; distance <= 4; distance++) {
|
||||
for (int height = -2; height <= 1; height++) {
|
||||
final Location result = JumpCalculator.validateAndRetryJump(
|
||||
current, heading, distance, height, new Random(distance * 10 + height), DEFAULT_SD);
|
||||
|
||||
assertTrue(validator.canJump(current, result),
|
||||
"unreachable block for distance=" + distance + " height=" + height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateAndRetryJumpReturnsCurrentAsLastResort() {
|
||||
final Location result = JumpCalculator.validateAndRetryJump(
|
||||
current, new Vector(100, 0, 0), 4, 1, new Random(7), DEFAULT_SD);
|
||||
|
||||
assertEquals(current, result);
|
||||
}
|
||||
|
||||
private static double dot(Vector a, Vector b) {
|
||||
return a.getX() * b.getX() + a.getY() * b.getY() + a.getZ() * b.getZ();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package dev.loki.loparkour.leaderboard.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* Unit tests for Score time parsing and serialization.
|
||||
*/
|
||||
class ScoreTest {
|
||||
|
||||
private static final String UNKNOWN_TIME = "?";
|
||||
|
||||
@Test
|
||||
void testGetTimeMillis_MinutesSeconds() {
|
||||
final Score score = new Score("player", "3:05.123", "1", 10);
|
||||
assertEquals(185123L, score.getTimeMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTimeMillis_HoursMinutesSeconds() {
|
||||
final Score score = new Score("player", "1:03:05.123", "1", 10);
|
||||
assertEquals(3785123L, score.getTimeMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTimeMillis_MinutesOver59() {
|
||||
final Score score = new Score("player", "75:05.123", "1", 10);
|
||||
assertEquals(4505123L, score.getTimeMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTimeMillis_ZeroTime() {
|
||||
final Score score = new Score("player", "0:10.000", "1", 10);
|
||||
assertEquals(10000L, score.getTimeMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTimeMillis_UnknownTime() {
|
||||
final Score score = new Score("player", UNKNOWN_TIME, "1", 10);
|
||||
assertEquals(Long.MAX_VALUE, score.getTimeMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTimeMillis_MillisLessMinutesSeconds() {
|
||||
final Score score = new Score("player", "3:05", "1", 10);
|
||||
assertEquals(185000L, score.getTimeMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTimeMillis_MillisLessHoursMinutesSeconds() {
|
||||
final Score score = new Score("player", "1:2:3", "1", 10);
|
||||
assertEquals(3723000L, score.getTimeMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTimeMillis_Malformed() {
|
||||
assertEquals(Long.MAX_VALUE, new Score("player", "abc", "1", 10).getTimeMillis());
|
||||
assertEquals(Long.MAX_VALUE, new Score("player", "", "1", 10).getTimeMillis());
|
||||
assertEquals(Long.MAX_VALUE, new Score("player", "3:05:10.123.456", "1", 10).getTimeMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFromString_ToString_RoundTrip() {
|
||||
final Score score = new Score("player", "3:05.123", "Hard", 1500);
|
||||
assertEquals(score, Score.fromString(score.toString()));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package dev.loki.loparkour.schematic.core;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ParkourSchematic#isSaneSize(double, double, double)}.
|
||||
*/
|
||||
class ParkourSchematicSizeTest {
|
||||
|
||||
@Test
|
||||
void acceptsSaneDimensions() {
|
||||
assertTrue(ParkourSchematic.isSaneSize(1, 1, 1));
|
||||
assertTrue(ParkourSchematic.isSaneSize(10, 5, 4));
|
||||
assertTrue(ParkourSchematic.isSaneSize(ParkourSchematic.MAX_SCHEMATIC_EDGE, 1, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsZeroDimensions() {
|
||||
assertFalse(ParkourSchematic.isSaneSize(0, 5, 4));
|
||||
assertFalse(ParkourSchematic.isSaneSize(5, 0, 4));
|
||||
assertFalse(ParkourSchematic.isSaneSize(5, 4, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNegativeDimensions() {
|
||||
assertFalse(ParkourSchematic.isSaneSize(-1, 5, 4));
|
||||
assertFalse(ParkourSchematic.isSaneSize(5, -2, 4));
|
||||
assertFalse(ParkourSchematic.isSaneSize(5, 4, -3));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsOversizedEdge() {
|
||||
assertFalse(ParkourSchematic.isSaneSize(ParkourSchematic.MAX_SCHEMATIC_EDGE + 1, 1, 1));
|
||||
assertFalse(ParkourSchematic.isSaneSize(1, ParkourSchematic.MAX_SCHEMATIC_EDGE + 1, 1));
|
||||
assertFalse(ParkourSchematic.isSaneSize(1, 1, ParkourSchematic.MAX_SCHEMATIC_EDGE + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsTooManyBlocks() {
|
||||
assertFalse(ParkourSchematic.isSaneSize(100, 100, 100));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue