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(); + } + } + +}