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
- 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

View File

@ -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!)

View File

@ -706,6 +706,11 @@ public final class ConfigKeys {
*/
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
*/

View File

@ -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 {

View File

@ -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> keyPair;
private final Supplier<CompletableFuture<KeyPair>> 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<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() {
@ -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();
}
}

View File

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

View File

@ -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')
}

View File

@ -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);
}

View File

@ -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 <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.
*/
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;
* </ul>
* </p>
*/
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);
}

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