diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f5b4ba4c9..b84bce2f7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,13 +32,15 @@ jobs:
uses: gradle/gradle-build-action@v2
- name: Run build and tests with Gradle wrapper
- run: ./gradlew test build
+ run: ./gradlew test build -PdockerTests
- name: Publish test report
uses: mikepenz/action-junit-report@v3
if: success() || failure()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
+ annotate_notice: true
+ detailed_summary: true
- name: Upload all artifacts
uses: actions/upload-artifact@v3
diff --git a/README.md b/README.md
index 2d266a359..2dae793b8 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,12 @@ cd LuckPerms/
You can find the output jars in the `loader/build/libs` or `build/libs` directories.
+## Tests
+There are some automated tests which run during each build.
+
+* Unit tests are defined in [`common/src/test`](https://github.com/LuckPerms/LuckPerms/tree/master/common/src/test)
+* Integration tests are defined in [`standalone/src/test`](https://github.com/LuckPerms/LuckPerms/tree/master/standalone/src/test).
+
## Contributing
#### Pull Requests
If you make any changes or improvements to the plugin which you think would be beneficial to others, please consider making a pull request to merge your changes back into the upstream project. (especially if your changes are bug fixes!)
diff --git a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java
index 020da0c6c..36b45a44a 100644
--- a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java
+++ b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java
@@ -706,6 +706,11 @@ public final class ConfigKeys {
*/
public static final ConfigKey RABBITMQ_PASSWORD = notReloadable(stringKey("rabbitmq.password", "guest"));
+ /**
+ * If the editor key should be generated lazily (only when needed)
+ */
+ public static final ConfigKey EDITOR_LAZILY_GENERATE_KEY = booleanKey("editor-lazily-generate-key", false);
+
/**
* The URL of the bytebin instance used to upload data
*/
diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/rabbitmq/RabbitMQMessenger.java b/common/src/main/java/me/lucko/luckperms/common/messaging/rabbitmq/RabbitMQMessenger.java
index 6d84deb2c..b766c9872 100644
--- a/common/src/main/java/me/lucko/luckperms/common/messaging/rabbitmq/RabbitMQMessenger.java
+++ b/common/src/main/java/me/lucko/luckperms/common/messaging/rabbitmq/RabbitMQMessenger.java
@@ -37,6 +37,7 @@ import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
+import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask;
import net.luckperms.api.messenger.IncomingMessageConsumer;
import net.luckperms.api.messenger.Messenger;
@@ -44,6 +45,8 @@ import net.luckperms.api.messenger.message.OutgoingMessage;
import org.checkerframework.checker.nullness.qual.NonNull;
+import java.util.concurrent.TimeUnit;
+
/**
* An implementation of {@link Messenger} using RabbitMQ.
*/
@@ -62,6 +65,7 @@ public class RabbitMQMessenger implements Messenger {
private Connection connection;
private Channel channel;
private Subscription sub;
+ private SchedulerTask checkConnectionTask;
public RabbitMQMessenger(LuckPermsPlugin plugin, IncomingMessageConsumer consumer) {
this.plugin = plugin;
@@ -81,7 +85,8 @@ public class RabbitMQMessenger implements Messenger {
this.connectionFactory.setPassword(password);
this.sub = new Subscription();
- this.plugin.getBootstrap().getScheduler().executeAsync(this.sub);
+ checkAndReopenConnection(true);
+ this.checkConnectionTask = this.plugin.getBootstrap().getScheduler().asyncRepeating(() -> checkAndReopenConnection(false), 5, TimeUnit.SECONDS);
}
@Override
@@ -100,7 +105,7 @@ public class RabbitMQMessenger implements Messenger {
try {
this.channel.close();
this.connection.close();
- this.sub.isClosed = true;
+ this.checkConnectionTask.cancel();
} catch (Exception e) {
e.printStackTrace();
}
@@ -159,30 +164,7 @@ public class RabbitMQMessenger implements Messenger {
}
}
- private class Subscription implements Runnable, DeliverCallback {
- private boolean isClosed = false;
-
- @Override
- public void run() {
- boolean firstStartup = true;
- while (!Thread.interrupted() && !this.isClosed) {
- try {
- if (!checkAndReopenConnection(firstStartup)) {
- // Sleep for 5 seconds to prevent massive spam in console
- Thread.sleep(5000);
- continue;
- }
-
- // Check connection life every every 30 seconds
- Thread.sleep(30_000);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- } finally {
- firstStartup = false;
- }
- }
- }
-
+ private class Subscription implements DeliverCallback {
@Override
public void handle(String consumerTag, Delivery message) {
try {
diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java
index 26d28e846..421dc3fce 100644
--- a/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java
+++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java
@@ -25,6 +25,10 @@
package me.lucko.luckperms.common.webeditor.store;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+
+import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.webeditor.socket.CryptographyUtils;
@@ -35,17 +39,29 @@ import java.util.concurrent.CompletableFuture;
* Contains a store of known web editor sessions and provides a lookup function for
* trusted editor public keys.
*/
+@SuppressWarnings("Guava")
public class WebEditorStore {
private final WebEditorSessionMap sessions;
private final WebEditorSocketMap sockets;
private final WebEditorKeystore keystore;
- private final CompletableFuture keyPair;
+ private final Supplier> keyPair;
public WebEditorStore(LuckPermsPlugin plugin) {
this.sessions = new WebEditorSessionMap();
this.sockets = new WebEditorSocketMap();
this.keystore = new WebEditorKeystore(plugin.getBootstrap().getConfigDirectory().resolve("editor-keystore.json"));
- this.keyPair = CompletableFuture.supplyAsync(CryptographyUtils::generateKeyPair, plugin.getBootstrap().getScheduler().async());
+
+ Supplier> keyPair = () -> CompletableFuture.supplyAsync(
+ CryptographyUtils::generateKeyPair,
+ plugin.getBootstrap().getScheduler().async()
+ );
+
+ if (plugin.getConfiguration().get(ConfigKeys.EDITOR_LAZILY_GENERATE_KEY)) {
+ this.keyPair = Suppliers.memoize(keyPair);
+ } else {
+ CompletableFuture future = keyPair.get();
+ this.keyPair = () -> future;
+ }
}
public WebEditorSessionMap sessions() {
@@ -61,10 +77,10 @@ public class WebEditorStore {
}
public KeyPair keyPair() {
- if (!this.keyPair.isDone()) {
+ if (!this.keyPair.get().isDone()) {
throw new IllegalStateException("Web editor keypair has not been generated yet! Has the server just started?");
}
- return this.keyPair.join();
+ return this.keyPair.get().join();
}
}
diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java
index 27cfdd0f1..312611e4e 100644
--- a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java
+++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java
@@ -130,6 +130,10 @@ public class LuckPermsApplication implements AutoCloseable {
this.healthReporter = healthReporter;
}
+ public LuckPerms getApi() {
+ return this.luckPermsApi;
+ }
+
public CommandExecutor getCommandExecutor() {
return this.commandExecutor;
}
diff --git a/standalone/build.gradle b/standalone/build.gradle
index d1e0359f7..df177be6d 100644
--- a/standalone/build.gradle
+++ b/standalone/build.gradle
@@ -6,7 +6,11 @@ sourceCompatibility = 17
targetCompatibility = 17
test {
- useJUnitPlatform {}
+ useJUnitPlatform {
+ if (!project.hasProperty('dockerTests')) {
+ excludeTags 'docker'
+ }
+ }
}
dependencies {
@@ -19,9 +23,18 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.1'
+ testImplementation "org.testcontainers:junit-jupiter:1.17.6"
testImplementation 'org.mockito:mockito-core:4.11.0'
testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0'
+
testImplementation 'com.h2database:h2:2.1.214'
+ testImplementation 'mysql:mysql-connector-java:8.0.23'
+ testImplementation 'org.mariadb.jdbc:mariadb-java-client:2.7.2'
+ testImplementation 'org.postgresql:postgresql:42.2.19'
+ testImplementation 'org.mongodb:mongodb-driver-sync:4.5.0'
+ testImplementation 'me.lucko.configurate:configurate-toml:3.7'
+ testImplementation 'org.spongepowered:configurate-hocon:3.7.2'
+
testImplementation project(':standalone:app')
testImplementation project(':common:loader-utils')
}
diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandaloneBootstrap.java b/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandaloneBootstrap.java
index 1aa76dc84..2287b5b19 100644
--- a/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandaloneBootstrap.java
+++ b/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandaloneBootstrap.java
@@ -72,7 +72,7 @@ public class LPStandaloneBootstrap implements LuckPermsBootstrap, LoaderBootstra
}
@VisibleForTesting
- LPStandaloneBootstrap(LuckPermsApplication loader, ClassPathAppender classPathAppender) {
+ protected LPStandaloneBootstrap(LuckPermsApplication loader, ClassPathAppender classPathAppender) {
this.loader = loader;
this.logger = new Log4jPluginLogger(LuckPermsApplication.LOGGER);
@@ -82,7 +82,7 @@ public class LPStandaloneBootstrap implements LuckPermsBootstrap, LoaderBootstra
}
@VisibleForTesting
- LPStandalonePlugin createTestPlugin() {
+ protected LPStandalonePlugin createTestPlugin() {
return new LPStandalonePlugin(this);
}
diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/StandaloneIntegrationTests.java b/standalone/src/test/java/me/lucko/luckperms/standalone/IntegrationTest.java
similarity index 75%
rename from standalone/src/test/java/me/lucko/luckperms/standalone/StandaloneIntegrationTests.java
rename to standalone/src/test/java/me/lucko/luckperms/standalone/IntegrationTest.java
index 28b48d4b2..9481d1b55 100644
--- a/standalone/src/test/java/me/lucko/luckperms/standalone/StandaloneIntegrationTests.java
+++ b/standalone/src/test/java/me/lucko/luckperms/standalone/IntegrationTest.java
@@ -28,9 +28,9 @@ package me.lucko.luckperms.standalone;
import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.model.Group;
import me.lucko.luckperms.common.node.types.Permission;
-import me.lucko.luckperms.standalone.app.LuckPermsApplication;
import me.lucko.luckperms.standalone.app.integration.CommandExecutor;
import me.lucko.luckperms.standalone.app.integration.HealthReporter;
+import me.lucko.luckperms.standalone.utils.TestPluginProvider;
import net.luckperms.api.model.data.DataType;
import net.luckperms.api.node.NodeEqualityPredicate;
@@ -49,13 +49,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* A set of 'integration tests' for the standalone LuckPerms app.
*/
-public class StandaloneIntegrationTests {
-
- private @TempDir Path tempDir;
+public class IntegrationTest {
@Test
- public void testLoadEnableDisable() {
- useTestPlugin((app, bootstrap, plugin) -> {
+ public void testLoadEnableDisable(@TempDir Path tempDir) {
+ TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> {
HealthReporter.Health health = app.getHealthReporter().poll();
assertNotNull(health);
assertTrue(health.isUp());
@@ -63,8 +61,8 @@ public class StandaloneIntegrationTests {
}
@Test
- public void testRunCommand() {
- useTestPlugin((app, bootstrap, plugin) -> {
+ public void testRunCommand(@TempDir Path tempDir) {
+ TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> {
CommandExecutor commandExecutor = app.getCommandExecutor();
commandExecutor.execute("group default permission set test").join();
@@ -75,15 +73,15 @@ public class StandaloneIntegrationTests {
}
@Test
- public void testReloadConfig() throws IOException {
- useTestPlugin((app, bootstrap, plugin) -> {
+ public void testReloadConfig(@TempDir Path tempDir) throws IOException {
+ TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> {
String server = plugin.getConfiguration().get(ConfigKeys.SERVER);
assertEquals("global", server);
Integer syncTime = plugin.getConfiguration().get(ConfigKeys.SYNC_TIME);
assertEquals(-1, syncTime);
- Path config = this.tempDir.resolve("config.yml");
+ Path config = tempDir.resolve("config.yml");
assertTrue(Files.exists(config));
String configString = Files.readString(config)
@@ -101,22 +99,4 @@ public class StandaloneIntegrationTests {
});
}
- private void useTestPlugin(TestPluginConsumer consumer) throws E {
- LuckPermsApplication app = new LuckPermsApplication(() -> {});
- LPStandaloneTestBootstrap bootstrap = new LPStandaloneTestBootstrap(app, this.tempDir);
-
- bootstrap.onLoad();
- bootstrap.onEnable();
-
- try {
- consumer.accept(app, bootstrap, bootstrap.getPlugin());
- } finally {
- bootstrap.onDisable();
- }
- }
-
- interface TestPluginConsumer {
- void accept(LuckPermsApplication app, LPStandaloneTestBootstrap bootstrap, LPStandalonePlugin plugin) throws E;
- }
-
}
diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java
new file mode 100644
index 000000000..920697271
--- /dev/null
+++ b/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java
@@ -0,0 +1,184 @@
+/*
+ * This file is part of LuckPerms, licensed under the MIT License.
+ *
+ * Copyright (c) lucko (Luck)
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.lucko.luckperms.standalone;
+
+import com.google.common.collect.ImmutableMap;
+
+import me.lucko.luckperms.common.messaging.InternalMessagingService;
+import me.lucko.luckperms.standalone.app.integration.HealthReporter;
+import me.lucko.luckperms.standalone.utils.TestPluginProvider;
+
+import net.luckperms.api.event.sync.PreNetworkSyncEvent;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Testcontainers
+@Tag("docker")
+public class MessagingIntegrationTest {
+
+ private static void testMessaging(Map config, Path tempDirA, Path tempDirB) throws InterruptedException {
+ try (TestPluginProvider.Plugin pluginA = TestPluginProvider.create(tempDirA, config);
+ TestPluginProvider.Plugin pluginB = TestPluginProvider.create(tempDirB, config)) {
+
+ // check the plugins are healthy
+ HealthReporter.Health healthA = pluginA.app().getHealthReporter().poll();
+ assertNotNull(healthA);
+ assertTrue(healthA.isUp());
+
+ HealthReporter.Health healthB = pluginB.app().getHealthReporter().poll();
+ assertNotNull(healthB);
+ assertTrue(healthB.isUp());
+
+ InternalMessagingService messagingServiceA = pluginA.plugin().getMessagingService().orElse(null);
+ InternalMessagingService messagingServiceB = pluginB.plugin().getMessagingService().orElse(null);
+ assertNotNull(messagingServiceA);
+ assertNotNull(messagingServiceB);
+
+ CountDownLatch latch = new CountDownLatch(1);
+ pluginB.app().getApi().getEventBus().subscribe(PreNetworkSyncEvent.class, e -> latch.countDown());
+
+ // send a message from plugin A to plugin B and wait for the message to be received
+ messagingServiceA.pushUpdate();
+ assertTrue(latch.await(30, TimeUnit.SECONDS));
+ }
+ }
+
+ @Nested
+ class Sql {
+
+ @Container
+ private final GenericContainer> container = new GenericContainer<>(DockerImageName.parse("mysql:8"))
+ .withEnv("MYSQL_DATABASE", "minecraft")
+ .withEnv("MYSQL_ROOT_PASSWORD", "passw0rd")
+ .withExposedPorts(3306);
+
+ @Test
+ public void testMySql(@TempDir Path tempDirA, @TempDir Path tempDirB) throws InterruptedException {
+ assertTrue(this.container.isRunning());
+
+ String host = this.container.getHost();
+ Integer port = this.container.getFirstMappedPort();
+
+ Map config = ImmutableMap.builder()
+ .put("storage-method", "mysql")
+ .put("data.address", host + ":" + port)
+ .put("data.database", "minecraft")
+ .put("data.username", "root")
+ .put("data.password", "passw0rd")
+ .build();
+
+ testMessaging(config, tempDirA, tempDirB);
+ }
+ }
+
+ @Nested
+ class Redis {
+
+ @Container
+ private final GenericContainer> container = new GenericContainer<>(DockerImageName.parse("redis"))
+ .withExposedPorts(6379);
+
+ @Test
+ public void testRedis(@TempDir Path tempDirA, @TempDir Path tempDirB) throws InterruptedException {
+ assertTrue(this.container.isRunning());
+
+ String host = this.container.getHost();
+ Integer port = this.container.getFirstMappedPort();
+
+ Map config = ImmutableMap.builder()
+ .put("messaging-service", "redis")
+ .put("redis.enabled", "true")
+ .put("redis.address", host + ":" + port)
+ .build();
+
+ testMessaging(config, tempDirA, tempDirB);
+ }
+ }
+
+ @Nested
+ class RabbitMq {
+
+ @Container
+ private final GenericContainer> container = new GenericContainer<>(DockerImageName.parse("rabbitmq"))
+ .withExposedPorts(5672);
+
+ @Test
+ public void testRabbitMq(@TempDir Path tempDirA, @TempDir Path tempDirB) throws InterruptedException {
+ assertTrue(this.container.isRunning());
+
+ String host = this.container.getHost();
+ Integer port = this.container.getFirstMappedPort();
+
+ Map config = ImmutableMap.builder()
+ .put("messaging-service", "rabbitmq")
+ .put("rabbitmq.enabled", "true")
+ .put("rabbitmq.address", host + ":" + port)
+ .build();
+
+ testMessaging(config, tempDirA, tempDirB);
+ }
+ }
+
+ @Nested
+ class Nats {
+
+ @Container
+ private final GenericContainer> container = new GenericContainer<>(DockerImageName.parse("nats"))
+ .withExposedPorts(4222);
+
+ @Test
+ public void testNats(@TempDir Path tempDirA, @TempDir Path tempDirB) throws InterruptedException {
+ assertTrue(this.container.isRunning());
+
+ String host = this.container.getHost();
+ Integer port = this.container.getFirstMappedPort();
+
+ Map config = ImmutableMap.builder()
+ .put("messaging-service", "nats")
+ .put("nats.enabled", "true")
+ .put("nats.address", host + ":" + port)
+ .build();
+
+ testMessaging(config, tempDirA, tempDirB);
+ }
+ }
+
+}
diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java
new file mode 100644
index 000000000..50e759e0b
--- /dev/null
+++ b/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java
@@ -0,0 +1,260 @@
+/*
+ * This file is part of LuckPerms, licensed under the MIT License.
+ *
+ * Copyright (c) lucko (Luck)
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.lucko.luckperms.standalone;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import me.lucko.luckperms.common.model.Group;
+import me.lucko.luckperms.common.node.types.Inheritance;
+import me.lucko.luckperms.common.node.types.Permission;
+import me.lucko.luckperms.standalone.app.LuckPermsApplication;
+import me.lucko.luckperms.standalone.app.integration.HealthReporter;
+import me.lucko.luckperms.standalone.utils.TestPluginBootstrap;
+import me.lucko.luckperms.standalone.utils.TestPluginBootstrap.TestPlugin;
+import me.lucko.luckperms.standalone.utils.TestPluginProvider;
+
+import net.luckperms.api.event.cause.CreationCause;
+import net.luckperms.api.model.data.DataType;
+import net.luckperms.api.node.Node;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Testcontainers
+@Tag("docker")
+public class StorageIntegrationTest {
+
+ private static final Node TEST_PERMISSION = Permission.builder()
+ .permission("test")
+ .value(false)
+ .expiry(1, TimeUnit.HOURS)
+ .withContext("server", "foo")
+ .withContext("world", "bar")
+ .withContext("test", "test")
+ .build();
+
+ private static final Node TEST_GROUP = Inheritance.builder()
+ .group("default")
+ .value(false)
+ .expiry(1, TimeUnit.HOURS)
+ .withContext("server", "foo")
+ .withContext("world", "bar")
+ .withContext("test", "test")
+ .build();
+
+
+ private static void testStorage(LuckPermsApplication app, TestPluginBootstrap bootstrap, TestPlugin plugin) {
+ // check the plugin is healthy
+ HealthReporter.Health health = app.getHealthReporter().poll();
+ assertNotNull(health);
+ assertTrue(health.isUp());
+
+ // try to create / save a group
+ Group group = plugin.getStorage().createAndLoadGroup("test", CreationCause.INTERNAL).join();
+ group.setNode(DataType.NORMAL, TEST_PERMISSION, true);
+ group.setNode(DataType.NORMAL, TEST_GROUP, true);
+ plugin.getStorage().saveGroup(group).join();
+
+ plugin.getStorage().loadAllGroups().join();
+
+ Group testGroup = plugin.getGroupManager().getIfLoaded("test");
+ assertNotNull(testGroup);
+
+ assertEquals(ImmutableSet.of(TEST_PERMISSION, TEST_GROUP), testGroup.normalData().asSet());
+ }
+
+ @Nested
+ class MySql {
+
+ @Container
+ private final GenericContainer> container = new GenericContainer<>(DockerImageName.parse("mysql:8"))
+ .withEnv("MYSQL_DATABASE", "minecraft")
+ .withEnv("MYSQL_ROOT_PASSWORD", "passw0rd")
+ .withExposedPorts(3306);
+
+ @Test
+ public void testMySql(@TempDir Path tempDir) {
+ assertTrue(this.container.isRunning());
+
+ String host = this.container.getHost();
+ Integer port = this.container.getFirstMappedPort();
+
+ Map config = ImmutableMap.builder()
+ .put("storage-method", "mysql")
+ .put("data.address", host + ":" + port)
+ .put("data.database", "minecraft")
+ .put("data.username", "root")
+ .put("data.password", "passw0rd")
+ .build();
+
+ TestPluginProvider.use(tempDir, config, StorageIntegrationTest::testStorage);
+ }
+ }
+
+ @Nested
+ class MariaDb {
+
+ @Container
+ private final GenericContainer> container = new GenericContainer<>(DockerImageName.parse("mariadb"))
+ .withEnv("MARIADB_USER", "minecraft")
+ .withEnv("MARIADB_PASSWORD", "passw0rd")
+ .withEnv("MARIADB_ROOT_PASSWORD", "rootpassw0rd")
+ .withEnv("MARIADB_DATABASE", "minecraft")
+ .withExposedPorts(3306);
+
+ @Test
+ public void testMariaDb(@TempDir Path tempDir) {
+ assertTrue(this.container.isRunning());
+
+ String host = this.container.getHost();
+ Integer port = this.container.getFirstMappedPort();
+
+ Map config = ImmutableMap.builder()
+ .put("storage-method", "mariadb")
+ .put("data.address", host + ":" + port)
+ .put("data.database", "minecraft")
+ .put("data.username", "minecraft")
+ .put("data.password", "passw0rd")
+ .build();
+
+ TestPluginProvider.use(tempDir, config, StorageIntegrationTest::testStorage);
+ }
+ }
+
+ @Nested
+ class Postgres {
+
+ @Container
+ private final GenericContainer> container = new GenericContainer<>(DockerImageName.parse("postgres"))
+ .withEnv("POSTGRES_PASSWORD", "passw0rd")
+ .withExposedPorts(5432);
+
+ @Test
+ public void testPostgres(@TempDir Path tempDir) {
+ assertTrue(this.container.isRunning());
+
+ String host = this.container.getHost();
+ Integer port = this.container.getFirstMappedPort();
+
+ Map config = ImmutableMap.builder()
+ .put("storage-method", "postgresql")
+ .put("data.address", host + ":" + port)
+ .put("data.database", "postgres")
+ .put("data.username", "postgres")
+ .put("data.password", "passw0rd")
+ .build();
+
+ TestPluginProvider.use(tempDir, config, StorageIntegrationTest::testStorage);
+ }
+ }
+
+ @Nested
+ class MongoDb {
+
+ @Container
+ private final GenericContainer> container = new GenericContainer<>(DockerImageName.parse("mongo"))
+ .withExposedPorts(27017);
+
+ @Test
+ public void testMongo(@TempDir Path tempDir) {
+ assertTrue(this.container.isRunning());
+
+ String host = this.container.getHost();
+ Integer port = this.container.getFirstMappedPort();
+
+ Map config = ImmutableMap.builder()
+ .put("storage-method", "mongodb")
+ .put("data.address", host + ":" + port)
+ .put("data.database", "minecraft")
+ .put("data.username", "")
+ .put("data.password", "")
+ .build();
+
+ TestPluginProvider.use(tempDir, config, StorageIntegrationTest::testStorage);
+ }
+ }
+
+ @Nested
+ class FlatFile {
+
+ @Test
+ public void testYaml(@TempDir Path tempDir) {
+ TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "yaml"), StorageIntegrationTest::testStorage);
+ }
+
+ @Test
+ public void testJson(@TempDir Path tempDir) {
+ TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "json"), StorageIntegrationTest::testStorage);
+ }
+
+ @Test
+ public void testHocon(@TempDir Path tempDir) {
+ TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "hocon"), StorageIntegrationTest::testStorage);
+ }
+
+ @Test
+ public void testToml(@TempDir Path tempDir) {
+ TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "toml"), StorageIntegrationTest::testStorage);
+ }
+
+ @Test
+ public void testYamlCombined(@TempDir Path tempDir) {
+ TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "yaml-combined"), StorageIntegrationTest::testStorage);
+ }
+
+ @Test
+ public void testJsonCombined(@TempDir Path tempDir) {
+ TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "json-combined"), StorageIntegrationTest::testStorage);
+ }
+
+ @Test
+ public void testHoconCombined(@TempDir Path tempDir) {
+ TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "hocon-combined"), StorageIntegrationTest::testStorage);
+ }
+
+ @Test
+ public void testTomlCombined(@TempDir Path tempDir) {
+ TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "toml-combined"), StorageIntegrationTest::testStorage);
+ }
+
+ }
+
+}
diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/LPStandaloneTestBootstrap.java b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginBootstrap.java
similarity index 83%
rename from standalone/src/test/java/me/lucko/luckperms/standalone/LPStandaloneTestBootstrap.java
rename to standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginBootstrap.java
index 23639f8b6..a6b72939e 100644
--- a/standalone/src/test/java/me/lucko/luckperms/standalone/LPStandaloneTestBootstrap.java
+++ b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginBootstrap.java
@@ -23,12 +23,14 @@
* SOFTWARE.
*/
-package me.lucko.luckperms.standalone;
+package me.lucko.luckperms.standalone.utils;
import me.lucko.luckperms.common.dependencies.Dependency;
import me.lucko.luckperms.common.dependencies.DependencyManager;
import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender;
import me.lucko.luckperms.common.storage.StorageType;
+import me.lucko.luckperms.standalone.LPStandaloneBootstrap;
+import me.lucko.luckperms.standalone.LPStandalonePlugin;
import me.lucko.luckperms.standalone.app.LuckPermsApplication;
import java.nio.file.Path;
@@ -45,18 +47,18 @@ import java.util.Set;
*
*
*/
-public final class LPStandaloneTestBootstrap extends LPStandaloneBootstrap {
+public final class TestPluginBootstrap extends LPStandaloneBootstrap {
private static final ClassPathAppender NOOP_APPENDER = file -> {};
private final Path dataDirectory;
- private LPStandaloneTestPlugin plugin;
+ private TestPlugin plugin;
- LPStandaloneTestBootstrap(LuckPermsApplication app, Path dataDirectory) {
+ TestPluginBootstrap(LuckPermsApplication app, Path dataDirectory) {
super(app, NOOP_APPENDER);
this.dataDirectory = dataDirectory;
}
- public LPStandaloneTestPlugin getPlugin() {
+ public TestPlugin getPlugin() {
return this.plugin;
}
@@ -66,14 +68,13 @@ public final class LPStandaloneTestBootstrap extends LPStandaloneBootstrap {
}
@Override
- LPStandalonePlugin createTestPlugin() {
- System.setProperty("luckperms.auto-install-translations", "false");
- this.plugin = new LPStandaloneTestPlugin(this);
+ protected LPStandalonePlugin createTestPlugin() {
+ this.plugin = new TestPlugin(this);
return this.plugin;
}
- static final class LPStandaloneTestPlugin extends LPStandalonePlugin {
- LPStandaloneTestPlugin(LPStandaloneBootstrap bootstrap) {
+ public static final class TestPlugin extends LPStandalonePlugin {
+ TestPlugin(LPStandaloneBootstrap bootstrap) {
super(bootstrap);
}
diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginProvider.java b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginProvider.java
new file mode 100644
index 000000000..42b18a702
--- /dev/null
+++ b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginProvider.java
@@ -0,0 +1,113 @@
+/*
+ * This file is part of LuckPerms, licensed under the MIT License.
+ *
+ * Copyright (c) lucko (Luck)
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.lucko.luckperms.standalone.utils;
+
+import me.lucko.luckperms.standalone.app.LuckPermsApplication;
+import me.lucko.luckperms.standalone.utils.TestPluginBootstrap.TestPlugin;
+
+import org.testcontainers.shaded.com.google.common.collect.ImmutableMap;
+
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+public final class TestPluginProvider {
+ private TestPluginProvider() {}
+
+ /**
+ * Creates a test LuckPerms plugin instance, loads/enables it, and returns it.
+ *
+ * @param tempDir the temporary directory to run the plugin in
+ * @param config the config to set
+ * @return the plugin
+ */
+ public static Plugin create(Path tempDir, Map config) {
+ Map props = new HashMap<>(config);
+ props.putIfAbsent("auto-install-translations", "false");
+ props.putIfAbsent("editor-lazily-generate-key", "true");
+
+ props.forEach((k, v) -> System.setProperty("luckperms." + k, v));
+
+ LuckPermsApplication app = new LuckPermsApplication(() -> {});
+ TestPluginBootstrap bootstrap = new TestPluginBootstrap(app, tempDir);
+
+ bootstrap.onLoad();
+ bootstrap.onEnable();
+
+ props.keySet().forEach((k) -> System.clearProperty("luckperms." + k));
+
+ return new Plugin(app, bootstrap, bootstrap.getPlugin());
+ }
+
+ /**
+ * Creates a test LuckPerms plugin instance, loads/enables it, and returns it.
+ *
+ * @param tempDir the temporary directory to run the plugin in
+ * @return the plugin
+ */
+ public static Plugin create(Path tempDir) {
+ return create(tempDir, ImmutableMap.of());
+ }
+
+ /**
+ * Creates a test LuckPerms plugin instance, loads/enables it, runs the consumer, then disables it.
+ *
+ * @param tempDir the temporary directory to run the plugin in
+ * @param config the config to set
+ * @param consumer the consumer
+ * @param the exception class thrown by the consumer
+ * @throws E exception
+ */
+ public static void use(Path tempDir, Map config, Consumer consumer) throws E {
+ try (Plugin plugin = create(tempDir, config)) {
+ consumer.accept(plugin.app, plugin.bootstrap, plugin.plugin);
+ }
+ }
+
+ /**
+ * Creates a test LuckPerms plugin instance, loads/enables it, runs the consumer, then disables it.
+ *
+ * @param tempDir the temporary directory to run the plugin in
+ * @param consumer the consumer
+ * @param the exception class thrown by the consumer
+ * @throws E exception
+ */
+ public static void use(Path tempDir, Consumer consumer) throws E {
+ use(tempDir, ImmutableMap.of(), consumer);
+ }
+
+ public interface Consumer {
+ void accept(LuckPermsApplication app, TestPluginBootstrap bootstrap, TestPlugin plugin) throws E;
+ }
+
+ public record Plugin(LuckPermsApplication app, TestPluginBootstrap bootstrap, TestPlugin plugin) implements AutoCloseable {
+ @Override
+ public void close() {
+ this.bootstrap.onDisable();
+ }
+ }
+
+}