Add opt-in Paper integration tests
This commit is contained in:
2
TODO.md
2
TODO.md
@@ -18,7 +18,7 @@
|
|||||||
| MET-014 | Done | Review player lookup logic and use exact or otherwise explicit name-matching behavior where appropriate. |
|
| MET-014 | Done | Review player lookup logic and use exact or otherwise explicit name-matching behavior where appropriate. |
|
||||||
| MET-015 | Done | Refactor remaining code that uses deprecated or removed Bukkit, Spigot, or Paper APIs so it is compatible with the target Paper API. |
|
| MET-015 | Done | Refactor remaining code that uses deprecated or removed Bukkit, Spigot, or Paper APIs so it is compatible with the target Paper API. |
|
||||||
| MET-016 | Done | Add fast unit tests for plot and home allocation, reservation validation, player lookup and UUID-based ownership, configuration persistence, command validation, and event behavior. Keep domain rules independent of a running Minecraft server and mock external plugin boundaries as needed. |
|
| MET-016 | Done | Add fast unit tests for plot and home allocation, reservation validation, player lookup and UUID-based ownership, configuration persistence, command validation, and event behavior. Keep domain rules independent of a running Minecraft server and mock external plugin boundaries as needed. |
|
||||||
| MET-017 | Ready | Add a separate Gradle `integrationTest` source set/task for tests requiring an actual Paper server and compatible WorldGuard/WorldEdit plugins. Verify plugin loading, command registration and dispatch, region-backed reservations and home allocation, and persistence across a server restart. Run it only when explicitly requested; keep it out of the normal `test` and `build` tasks. |
|
| MET-017 | Done | Add a separate Gradle `integrationTest` source set/task for tests requiring an actual Paper server and compatible WorldGuard/WorldEdit plugins. Verify plugin loading, command registration and dispatch, region-backed reservations and home allocation, and persistence across a server restart. Run it only when explicitly requested; keep it out of the normal `test` and `build` tasks. |
|
||||||
| MET-018 | Ready | Build the plugin with Gradle and use the provided server scripts to smoke-test it on modern Paper with its required plugins installed. |
|
| MET-018 | Ready | Build the plugin with Gradle and use the provided server scripts to smoke-test it on modern Paper with its required plugins installed. |
|
||||||
| MET-019 | Ready | Address bugs and incompatibilities found by unit tests, integration tests, or modern Paper server testing. |
|
| MET-019 | Ready | Address bugs and incompatibilities found by unit tests, integration tests, or modern Paper server testing. |
|
||||||
| MET-020 | Ready | Adopt the git-sensitive semantic versioning Gradle plugin used by MobScores and ScoreKeeper, using the existing 0.5-SNAPSHOT version as the migration baseline; verify version/tag behavior and enable the Gradle configuration cache when compatible. |
|
| MET-020 | Ready | Adopt the git-sensitive semantic versioning Gradle plugin used by MobScores and ScoreKeeper, using the existing 0.5-SNAPSHOT version as the migration baseline; verify version/tag behavior and enable the Gradle configuration cache when compatible. |
|
||||||
|
|||||||
21
build.gradle
21
build.gradle
@@ -27,6 +27,18 @@ dependencies {
|
|||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.13.4'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.13.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
integrationTest {
|
||||||
|
compileClasspath += sourceSets.main.output
|
||||||
|
runtimeClasspath += sourceSets.main.output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
integrationTestImplementation.extendsFrom testImplementation
|
||||||
|
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
|
||||||
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(25)
|
languageVersion = JavaLanguageVersion.of(25)
|
||||||
@@ -54,6 +66,15 @@ tasks.named('test') {
|
|||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register('integrationTest', Test) {
|
||||||
|
description = 'Runs opt-in tests against a running Paper server with RCON enabled.'
|
||||||
|
group = 'verification'
|
||||||
|
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||||
|
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||||
|
useJUnitPlatform()
|
||||||
|
shouldRunAfter tasks.named('test')
|
||||||
|
}
|
||||||
|
|
||||||
tasks.named('wrapper') {
|
tasks.named('wrapper') {
|
||||||
gradleVersion = '9.8.0'
|
gradleVersion = '9.8.0'
|
||||||
distributionType = Wrapper.DistributionType.BIN
|
distributionType = Wrapper.DistributionType.BIN
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class MetropolisServerIntegrationTest {
|
||||||
|
@Test
|
||||||
|
void runningPaperLoadsMetropolisAndDispatchesItsVersionCommand() throws IOException {
|
||||||
|
try (RconClient rcon = connectToConfiguredServer()) {
|
||||||
|
String installedPlugins = rcon.execute("plugins");
|
||||||
|
String versionResponse = rcon.execute("metropolis");
|
||||||
|
|
||||||
|
assertTrue(installedPlugins.contains("Metropolis"), installedPlugins);
|
||||||
|
assertTrue(versionResponse.contains("Metropolis: version"), versionResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void plotReservationIsPersistedByTheLoadedWorldGuardPlugin() throws IOException {
|
||||||
|
String regionId = "metropolis-it-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
try (RconClient rcon = connectToConfiguredServer()) {
|
||||||
|
String response = rcon.execute("metropolis-plot-reserve " + regionId + " 100 60 100 105 70 105");
|
||||||
|
assertTrue(response.contains("Reserved plot region " + regionId), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path serverDirectory = Path.of(requiredEnvironment("METROPOLIS_SERVER_DIR"));
|
||||||
|
String worldName = System.getenv().getOrDefault("METROPOLIS_WORLD", "world");
|
||||||
|
Path regionsFile = serverDirectory.resolve("plugins/WorldGuard/worlds")
|
||||||
|
.resolve(worldName)
|
||||||
|
.resolve("regions.yml");
|
||||||
|
YamlConfiguration regions = YamlConfiguration.loadConfiguration(regionsFile.toFile());
|
||||||
|
|
||||||
|
assertTrue(regions.contains("regions." + regionId), "WorldGuard did not persist region " + regionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RconClient connectToConfiguredServer() throws IOException {
|
||||||
|
String host = requiredEnvironment("METROPOLIS_RCON_HOST");
|
||||||
|
int port = Integer.parseInt(requiredEnvironment("METROPOLIS_RCON_PORT"));
|
||||||
|
String password = requiredEnvironment("METROPOLIS_RCON_PASSWORD");
|
||||||
|
return new RconClient(host, port, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requiredEnvironment(String name) {
|
||||||
|
String value = System.getenv(name);
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new IllegalStateException("Set " + name + " to run integrationTest.");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RconClient implements AutoCloseable {
|
||||||
|
private final Socket socket = new Socket();
|
||||||
|
private final DataInputStream input;
|
||||||
|
private final DataOutputStream output;
|
||||||
|
private int requestId = 1;
|
||||||
|
|
||||||
|
private RconClient(String host, int port, String password) throws IOException {
|
||||||
|
socket.connect(new InetSocketAddress(host, port), 5000);
|
||||||
|
socket.setSoTimeout(5000);
|
||||||
|
input = new DataInputStream(socket.getInputStream());
|
||||||
|
output = new DataOutputStream(socket.getOutputStream());
|
||||||
|
int authId = requestId++;
|
||||||
|
Packet response = send(authId, 3, password);
|
||||||
|
if (response.id != authId) {
|
||||||
|
throw new IOException("RCON authentication failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String execute(String command) throws IOException {
|
||||||
|
return send(requestId++, 2, command).payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Packet send(int id, int type, String payload) throws IOException {
|
||||||
|
byte[] data = payload.getBytes(StandardCharsets.UTF_8);
|
||||||
|
writeLittleEndianInt(data.length + 10);
|
||||||
|
writeLittleEndianInt(id);
|
||||||
|
writeLittleEndianInt(type);
|
||||||
|
output.write(data);
|
||||||
|
output.writeByte(0);
|
||||||
|
output.writeByte(0);
|
||||||
|
output.flush();
|
||||||
|
|
||||||
|
int length = readLittleEndianInt();
|
||||||
|
byte[] packet = input.readNBytes(length);
|
||||||
|
if (packet.length != length || length < 10) {
|
||||||
|
throw new IOException("Received an invalid RCON packet.");
|
||||||
|
}
|
||||||
|
int responseId = littleEndianInt(packet, 0);
|
||||||
|
String response = new String(packet, 8, length - 10, StandardCharsets.UTF_8);
|
||||||
|
return new Packet(responseId, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeLittleEndianInt(int value) throws IOException {
|
||||||
|
output.writeInt(Integer.reverseBytes(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readLittleEndianInt() throws IOException {
|
||||||
|
return Integer.reverseBytes(input.readInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int littleEndianInt(byte[] bytes, int offset) {
|
||||||
|
return (bytes[offset] & 0xff)
|
||||||
|
| ((bytes[offset + 1] & 0xff) << 8)
|
||||||
|
| ((bytes[offset + 2] & 0xff) << 16)
|
||||||
|
| (bytes[offset + 3] << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Packet(int id, String payload) {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user