Add integration tests for storage and messaging

This commit is contained in:
Luck 2023-04-01 21:34:44 +01:00
parent 6523e708a1
commit 4068c71d5a
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
13 changed files with 639 additions and 73 deletions

View File

@ -32,13 +32,15 @@ jobs:
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v2
- name: Run build and tests with Gradle wrapper - name: Run build and tests with Gradle wrapper
run: ./gradlew test build run: ./gradlew test build -PdockerTests
- name: Publish test report - name: Publish test report
uses: mikepenz/action-junit-report@v3 uses: mikepenz/action-junit-report@v3
if: success() || failure() if: success() || failure()
with: with:
report_paths: '**/build/test-results/test/TEST-*.xml' report_paths: '**/build/test-results/test/TEST-*.xml'
annotate_notice: true
detailed_summary: true
- name: Upload all artifacts - name: Upload all artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3

View File

@ -36,6 +36,12 @@ cd LuckPerms/
You can find the output jars in the `loader/build/libs` or `build/libs` directories. 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 ## Contributing
#### Pull Requests #### 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!) 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!)

View File

@ -706,6 +706,11 @@ public final class ConfigKeys {
*/ */
public static final ConfigKey<String> RABBITMQ_PASSWORD = notReloadable(stringKey("rabbitmq.password", "guest")); public static final ConfigKey<String> RABBITMQ_PASSWORD = notReloadable(stringKey("rabbitmq.password", "guest"));
/**
* If the editor key should be generated lazily (only when needed)
*/
public static final ConfigKey<Boolean> EDITOR_LAZILY_GENERATE_KEY = booleanKey("editor-lazily-generate-key", false);
/** /**
* The URL of the bytebin instance used to upload data * The URL of the bytebin instance used to upload data
*/ */

View File

@ -37,6 +37,7 @@ import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery; import com.rabbitmq.client.Delivery;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin; 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.IncomingMessageConsumer;
import net.luckperms.api.messenger.Messenger; 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 org.checkerframework.checker.nullness.qual.NonNull;
import java.util.concurrent.TimeUnit;
/** /**
* An implementation of {@link Messenger} using RabbitMQ. * An implementation of {@link Messenger} using RabbitMQ.
*/ */
@ -62,6 +65,7 @@ public class RabbitMQMessenger implements Messenger {
private Connection connection; private Connection connection;
private Channel channel; private Channel channel;
private Subscription sub; private Subscription sub;
private SchedulerTask checkConnectionTask;
public RabbitMQMessenger(LuckPermsPlugin plugin, IncomingMessageConsumer consumer) { public RabbitMQMessenger(LuckPermsPlugin plugin, IncomingMessageConsumer consumer) {
this.plugin = plugin; this.plugin = plugin;
@ -81,7 +85,8 @@ public class RabbitMQMessenger implements Messenger {
this.connectionFactory.setPassword(password); this.connectionFactory.setPassword(password);
this.sub = new Subscription(); 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 @Override
@ -100,7 +105,7 @@ public class RabbitMQMessenger implements Messenger {
try { try {
this.channel.close(); this.channel.close();
this.connection.close(); this.connection.close();
this.sub.isClosed = true; this.checkConnectionTask.cancel();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -159,30 +164,7 @@ public class RabbitMQMessenger implements Messenger {
} }
} }
private class Subscription implements Runnable, DeliverCallback { private class Subscription implements 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;
}
}
}
@Override @Override
public void handle(String consumerTag, Delivery message) { public void handle(String consumerTag, Delivery message) {
try { try {

View File

@ -25,6 +25,10 @@
package me.lucko.luckperms.common.webeditor.store; 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.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.webeditor.socket.CryptographyUtils; 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 * Contains a store of known web editor sessions and provides a lookup function for
* trusted editor public keys. * trusted editor public keys.
*/ */
@SuppressWarnings("Guava")
public class WebEditorStore { public class WebEditorStore {
private final WebEditorSessionMap sessions; private final WebEditorSessionMap sessions;
private final WebEditorSocketMap sockets; private final WebEditorSocketMap sockets;
private final WebEditorKeystore keystore; private final WebEditorKeystore keystore;
private final CompletableFuture<KeyPair> keyPair; private final Supplier<CompletableFuture<KeyPair>> keyPair;
public WebEditorStore(LuckPermsPlugin plugin) { public WebEditorStore(LuckPermsPlugin plugin) {
this.sessions = new WebEditorSessionMap(); this.sessions = new WebEditorSessionMap();
this.sockets = new WebEditorSocketMap(); this.sockets = new WebEditorSocketMap();
this.keystore = new WebEditorKeystore(plugin.getBootstrap().getConfigDirectory().resolve("editor-keystore.json")); this.keystore = new WebEditorKeystore(plugin.getBootstrap().getConfigDirectory().resolve("editor-keystore.json"));
this.keyPair = CompletableFuture.supplyAsync(CryptographyUtils::generateKeyPair, plugin.getBootstrap().getScheduler().async());
Supplier<CompletableFuture<KeyPair>> 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<KeyPair> future = keyPair.get();
this.keyPair = () -> future;
}
} }
public WebEditorSessionMap sessions() { public WebEditorSessionMap sessions() {
@ -61,10 +77,10 @@ public class WebEditorStore {
} }
public KeyPair keyPair() { 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?"); 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();
} }
} }

View File

@ -130,6 +130,10 @@ public class LuckPermsApplication implements AutoCloseable {
this.healthReporter = healthReporter; this.healthReporter = healthReporter;
} }
public LuckPerms getApi() {
return this.luckPermsApi;
}
public CommandExecutor getCommandExecutor() { public CommandExecutor getCommandExecutor() {
return this.commandExecutor; return this.commandExecutor;
} }

View File

@ -6,7 +6,11 @@ sourceCompatibility = 17
targetCompatibility = 17 targetCompatibility = 17
test { test {
useJUnitPlatform {} useJUnitPlatform {
if (!project.hasProperty('dockerTests')) {
excludeTags 'docker'
}
}
} }
dependencies { dependencies {
@ -19,9 +23,18 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1' 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-engine:5.9.1'
testImplementation 'org.junit.jupiter:junit-jupiter-params: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-core:4.11.0'
testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0' testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0'
testImplementation 'com.h2database:h2:2.1.214' 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(':standalone:app')
testImplementation project(':common:loader-utils') testImplementation project(':common:loader-utils')
} }

View File

@ -72,7 +72,7 @@ public class LPStandaloneBootstrap implements LuckPermsBootstrap, LoaderBootstra
} }
@VisibleForTesting @VisibleForTesting
LPStandaloneBootstrap(LuckPermsApplication loader, ClassPathAppender classPathAppender) { protected LPStandaloneBootstrap(LuckPermsApplication loader, ClassPathAppender classPathAppender) {
this.loader = loader; this.loader = loader;
this.logger = new Log4jPluginLogger(LuckPermsApplication.LOGGER); this.logger = new Log4jPluginLogger(LuckPermsApplication.LOGGER);
@ -82,7 +82,7 @@ public class LPStandaloneBootstrap implements LuckPermsBootstrap, LoaderBootstra
} }
@VisibleForTesting @VisibleForTesting
LPStandalonePlugin createTestPlugin() { protected LPStandalonePlugin createTestPlugin() {
return new LPStandalonePlugin(this); return new LPStandalonePlugin(this);
} }

View File

@ -28,9 +28,9 @@ package me.lucko.luckperms.standalone;
import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Group;
import me.lucko.luckperms.common.node.types.Permission; 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.CommandExecutor;
import me.lucko.luckperms.standalone.app.integration.HealthReporter; 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.model.data.DataType;
import net.luckperms.api.node.NodeEqualityPredicate; 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. * A set of 'integration tests' for the standalone LuckPerms app.
*/ */
public class StandaloneIntegrationTests { public class IntegrationTest {
private @TempDir Path tempDir;
@Test @Test
public void testLoadEnableDisable() { public void testLoadEnableDisable(@TempDir Path tempDir) {
useTestPlugin((app, bootstrap, plugin) -> { TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> {
HealthReporter.Health health = app.getHealthReporter().poll(); HealthReporter.Health health = app.getHealthReporter().poll();
assertNotNull(health); assertNotNull(health);
assertTrue(health.isUp()); assertTrue(health.isUp());
@ -63,8 +61,8 @@ public class StandaloneIntegrationTests {
} }
@Test @Test
public void testRunCommand() { public void testRunCommand(@TempDir Path tempDir) {
useTestPlugin((app, bootstrap, plugin) -> { TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> {
CommandExecutor commandExecutor = app.getCommandExecutor(); CommandExecutor commandExecutor = app.getCommandExecutor();
commandExecutor.execute("group default permission set test").join(); commandExecutor.execute("group default permission set test").join();
@ -75,15 +73,15 @@ public class StandaloneIntegrationTests {
} }
@Test @Test
public void testReloadConfig() throws IOException { public void testReloadConfig(@TempDir Path tempDir) throws IOException {
useTestPlugin((app, bootstrap, plugin) -> { TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> {
String server = plugin.getConfiguration().get(ConfigKeys.SERVER); String server = plugin.getConfiguration().get(ConfigKeys.SERVER);
assertEquals("global", server); assertEquals("global", server);
Integer syncTime = plugin.getConfiguration().get(ConfigKeys.SYNC_TIME); Integer syncTime = plugin.getConfiguration().get(ConfigKeys.SYNC_TIME);
assertEquals(-1, syncTime); assertEquals(-1, syncTime);
Path config = this.tempDir.resolve("config.yml"); Path config = tempDir.resolve("config.yml");
assertTrue(Files.exists(config)); assertTrue(Files.exists(config));
String configString = Files.readString(config) String configString = Files.readString(config)
@ -101,22 +99,4 @@ public class StandaloneIntegrationTests {
}); });
} }
private <E extends Throwable> void useTestPlugin(TestPluginConsumer<E> 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<E extends Throwable> {
void accept(LuckPermsApplication app, LPStandaloneTestBootstrap bootstrap, LPStandalonePlugin plugin) throws E;
}
} }

View File

@ -0,0 +1,184 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* 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<String, String> 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<String, String> config = ImmutableMap.<String, String>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<String, String> config = ImmutableMap.<String, String>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<String, String> config = ImmutableMap.<String, String>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<String, String> config = ImmutableMap.<String, String>builder()
.put("messaging-service", "nats")
.put("nats.enabled", "true")
.put("nats.address", host + ":" + port)
.build();
testMessaging(config, tempDirA, tempDirB);
}
}
}

View File

@ -0,0 +1,260 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* 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<String, String> config = ImmutableMap.<String, String>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<String, String> config = ImmutableMap.<String, String>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<String, String> config = ImmutableMap.<String, String>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<String, String> config = ImmutableMap.<String, String>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);
}
}
}

View File

@ -23,12 +23,14 @@
* SOFTWARE. * 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.Dependency;
import me.lucko.luckperms.common.dependencies.DependencyManager; import me.lucko.luckperms.common.dependencies.DependencyManager;
import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender;
import me.lucko.luckperms.common.storage.StorageType; 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 me.lucko.luckperms.standalone.app.LuckPermsApplication;
import java.nio.file.Path; import java.nio.file.Path;
@ -45,18 +47,18 @@ import java.util.Set;
* </ul> * </ul>
* </p> * </p>
*/ */
public final class LPStandaloneTestBootstrap extends LPStandaloneBootstrap { public final class TestPluginBootstrap extends LPStandaloneBootstrap {
private static final ClassPathAppender NOOP_APPENDER = file -> {}; private static final ClassPathAppender NOOP_APPENDER = file -> {};
private final Path dataDirectory; private final Path dataDirectory;
private LPStandaloneTestPlugin plugin; private TestPlugin plugin;
LPStandaloneTestBootstrap(LuckPermsApplication app, Path dataDirectory) { TestPluginBootstrap(LuckPermsApplication app, Path dataDirectory) {
super(app, NOOP_APPENDER); super(app, NOOP_APPENDER);
this.dataDirectory = dataDirectory; this.dataDirectory = dataDirectory;
} }
public LPStandaloneTestPlugin getPlugin() { public TestPlugin getPlugin() {
return this.plugin; return this.plugin;
} }
@ -66,14 +68,13 @@ public final class LPStandaloneTestBootstrap extends LPStandaloneBootstrap {
} }
@Override @Override
LPStandalonePlugin createTestPlugin() { protected LPStandalonePlugin createTestPlugin() {
System.setProperty("luckperms.auto-install-translations", "false"); this.plugin = new TestPlugin(this);
this.plugin = new LPStandaloneTestPlugin(this);
return this.plugin; return this.plugin;
} }
static final class LPStandaloneTestPlugin extends LPStandalonePlugin { public static final class TestPlugin extends LPStandalonePlugin {
LPStandaloneTestPlugin(LPStandaloneBootstrap bootstrap) { TestPlugin(LPStandaloneBootstrap bootstrap) {
super(bootstrap); super(bootstrap);
} }

View File

@ -0,0 +1,113 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* 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<String, String> config) {
Map<String, String> 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 <E> the exception class thrown by the consumer
* @throws E exception
*/
public static <E extends Throwable> void use(Path tempDir, Map<String, String> config, Consumer<E> 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 <E> the exception class thrown by the consumer
* @throws E exception
*/
public static <E extends Throwable> void use(Path tempDir, Consumer<E> consumer) throws E {
use(tempDir, ImmutableMap.of(), consumer);
}
public interface Consumer<E extends Throwable> {
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();
}
}
}