Add unit test for a message going from the receive event to a message sent to Discord + some other tests to check that enable works correctly

This commit is contained in:
Vankka 2023-07-24 22:04:41 +03:00
parent 3c3977200f
commit 84160b906f
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
13 changed files with 363 additions and 85 deletions

View File

@ -52,7 +52,6 @@ import com.discordsrv.common.groupsync.GroupSyncModule;
import com.discordsrv.common.invite.DiscordInviteModule;
import com.discordsrv.common.linking.LinkProvider;
import com.discordsrv.common.linking.LinkingModule;
import com.discordsrv.common.linking.impl.MemoryLinker;
import com.discordsrv.common.linking.impl.MinecraftAuthenticationLinker;
import com.discordsrv.common.linking.impl.StorageLinker;
import com.discordsrv.common.logging.Logger;
@ -75,6 +74,7 @@ import com.discordsrv.common.placeholder.result.ComponentResultStringifier;
import com.discordsrv.common.profile.ProfileManager;
import com.discordsrv.common.storage.Storage;
import com.discordsrv.common.storage.StorageType;
import com.discordsrv.common.storage.impl.MemoryStorage;
import com.discordsrv.common.update.UpdateChecker;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -139,7 +139,7 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
// Version
private UpdateChecker updateChecker;
private VersionInfo versionInfo;
protected VersionInfo versionInfo;
private OkHttpClient httpClient;
private final ObjectMapper objectMapper = new ObjectMapper()
@ -571,6 +571,9 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
case "mysql": return StorageType.MYSQL;
case "mariadb": return StorageType.MARIADB;
}
if (backend.equals(MemoryStorage.IDENTIFIER)) {
return StorageType.MEMORY;
}
throw new StorageException("Unknown storage backend \"" + backend + "\"");
}
@ -659,12 +662,6 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
linkProvider = new StorageLinker(this);
logger().info("Using storage for linked accounts");
break;
case "memory": {
linkProvider = new MemoryLinker();
logger().warning("Using memory for linked accounts");
logger().warning("Linked accounts will be lost upon restart");
break;
}
default: {
linkProvider = null;
logger().error("Unknown linked account provider: \"" + provider + "\", linked accounts will not be used");
@ -686,6 +683,10 @@ public abstract class AbstractDiscordSRV<B extends IBootstrap, C extends MainCon
try {
StorageType storageType = getStorageType();
logger().info("Using " + storageType.prettyName() + " as storage");
if (storageType == StorageType.MEMORY) {
logger().warning("Using memory as storage backend.");
logger().warning("Data will not persist across server restarts.");
}
if (storageType.hikari()) {
dependencyManager().hikari().download().get();
}

View File

@ -32,7 +32,7 @@ public class BaseChannelConfig {
public DiscordToMinecraftChatConfig discordToMinecraft = new DiscordToMinecraftChatConfig();
public JoinMessageConfig joinMessages() {
return null;
return new JoinMessageConfig();
}
@Order(2)

View File

@ -32,6 +32,7 @@ import com.discordsrv.common.debug.file.TextDebugFile;
import com.discordsrv.common.exception.InvalidListenerMethodException;
import com.discordsrv.common.logging.Logger;
import com.discordsrv.common.logging.NamedLogger;
import com.discordsrv.common.testing.TestHelper;
import net.dv8tion.jda.api.events.GenericEvent;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
@ -209,6 +210,7 @@ public class EventBusImpl implements EventBus {
eventListener.method().invoke(listener, event);
} catch (IllegalAccessException e) {
logger.error("Failed to access listener method: " + eventListener.methodName() + " in " + eventListener.className(), e);
TestHelper.fail(e);
} catch (InvocationTargetException e) {
String eventClassName = eventClass.getName();
Throwable cause = e.getCause();
@ -217,6 +219,7 @@ public class EventBusImpl implements EventBus {
} else {
e.getCause().printStackTrace();
}
TestHelper.fail(cause);
}
long timeTaken = System.currentTimeMillis() - startTime;
logger.trace(eventListener + " took " + timeTaken + "ms to execute");

View File

@ -1,61 +0,0 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2023 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.linking.impl;
import com.discordsrv.common.linking.LinkProvider;
import com.discordsrv.common.linking.LinkStore;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class MemoryLinker implements LinkProvider, LinkStore {
private final BidiMap<UUID, Long> map = new DualHashBidiMap<>();
@Override
public CompletableFuture<Optional<Long>> queryUserId(@NotNull UUID playerUUID) {
return CompletableFuture.completedFuture(Optional.ofNullable(map.get(playerUUID)));
}
@Override
public CompletableFuture<Optional<UUID>> queryPlayerUUID(long userId) {
return CompletableFuture.completedFuture(Optional.ofNullable(map.getKey(userId)));
}
@Override
public CompletableFuture<Void> createLink(@NotNull UUID playerUUID, long userId) {
map.put(playerUUID, userId);
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture<Void> removeLink(@NotNull UUID playerUUID, long userId) {
map.remove(playerUUID);
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture<Integer> getLinkedAccountCount() {
return CompletableFuture.completedFuture(map.size());
}
}

View File

@ -35,6 +35,7 @@ import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.discordsrv.common.logging.NamedLogger;
import com.discordsrv.common.module.type.AbstractModule;
import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.testing.TestHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -128,6 +129,7 @@ public abstract class AbstractGameMessageModule<T extends IMessageConfig, E exte
t = t.getCause();
}
ErrorCallbackContext.context("Failed to deliver a message to " + entry.getValue()).accept(t);
TestHelper.fail(t);
return null;
});
// Ignore ones that failed
@ -150,10 +152,12 @@ public abstract class AbstractGameMessageModule<T extends IMessageConfig, E exte
return null;
}
discordSRV.logger().error("Failed to publish to event bus", t);
TestHelper.fail(t);
return null;
});
}).exceptionally(t -> {
discordSRV.logger().error("Error in sending message", t);
TestHelper.fail(t);
return null;
});
}

View File

@ -19,6 +19,7 @@
package com.discordsrv.common.storage;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.storage.impl.MemoryStorage;
import com.discordsrv.common.storage.impl.sql.file.H2Storage;
import com.discordsrv.common.storage.impl.sql.hikari.MariaDBStorage;
import com.discordsrv.common.storage.impl.sql.hikari.MySQLStorage;
@ -29,7 +30,8 @@ public enum StorageType {
H2(H2Storage::new, "H2", false),
MYSQL(MySQLStorage::new, "MySQL", true),
MARIADB(MariaDBStorage::new, "MariaDB", true);
MARIADB(MariaDBStorage::new, "MariaDB", true),
MEMORY(discordSRV -> new MemoryStorage(), "Memory", false);
private final Function<DiscordSRV, Storage> storageFunction;
private final String prettyName;

View File

@ -0,0 +1,51 @@
package com.discordsrv.common.storage.impl;
import com.discordsrv.common.storage.Storage;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public class MemoryStorage implements Storage {
public static String IDENTIFIER = UUID.randomUUID().toString();
private final BidiMap<UUID, Long> linkedAccounts = new DualHashBidiMap<>();
public MemoryStorage() {}
@Override
public void initialize() {}
@Override
public void close() {
linkedAccounts.clear();
}
@Override
public @Nullable Long getUserId(@NotNull UUID player) {
return linkedAccounts.get(player);
}
@Override
public @Nullable UUID getPlayerUUID(long userId) {
return linkedAccounts.getKey(userId);
}
@Override
public void createLink(@NotNull UUID player, long userId) {
linkedAccounts.put(player, userId);
}
@Override
public void removeLink(@NotNull UUID player, long userId) {
linkedAccounts.remove(player, userId);
}
@Override
public int getLinkedAccountCount() {
return linkedAccounts.size();
}
}

View File

@ -0,0 +1,19 @@
package com.discordsrv.common.testing;
import java.util.function.Consumer;
public final class TestHelper {
private static final ThreadLocal<Consumer<Throwable>> error = new ThreadLocal<>();
public static void fail(Throwable throwable) {
Consumer<Throwable> handler = error.get();
if (handler != null) {
handler.accept(throwable);
}
}
public static void set(Consumer<Throwable> handler) {
error.set(handler);
}
}

View File

@ -0,0 +1,35 @@
package com.discordsrv.common;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.extension.*;
public class FullBootExtension implements BeforeAllCallback, ExtensionContext.Store.CloseableResource {
public static String BOT_TOKEN = System.getenv("DISCORDSRV_AUTOTEST_BOT_TOKEN");
public static String TEST_CHANNEL_ID = System.getenv("DISCORDSRV_AUTOTEST_CHANNEL_ID");
public boolean started = false;
@Override
public void beforeAll(ExtensionContext context) {
Assumptions.assumeTrue(BOT_TOKEN != null, "Automated testing bot token");
Assumptions.assumeTrue(TEST_CHANNEL_ID != null, "Automated testing channel id");
if (started) return;
started = true;
try {
System.out.println("Enabling...");
MockDiscordSRV.INSTANCE.enable();
System.out.println("Enabled successfully");
} catch (Throwable e) {
Assertions.fail(e);
}
}
@Override
public void close() {
MockDiscordSRV.INSTANCE.disable();
}
}

View File

@ -18,23 +18,31 @@
package com.discordsrv.common;
import com.discordsrv.api.channel.GameChannel;
import com.discordsrv.common.bootstrap.IBootstrap;
import com.discordsrv.common.bootstrap.LifecycleManager;
import com.discordsrv.common.command.game.handler.ICommandHandler;
import com.discordsrv.common.config.connection.ConnectionConfig;
import com.discordsrv.common.config.main.MainConfig;
import com.discordsrv.common.config.main.PluginIntegrationConfig;
import com.discordsrv.common.config.configurate.manager.ConnectionConfigManager;
import com.discordsrv.common.config.configurate.manager.MainConfigManager;
import com.discordsrv.common.config.configurate.manager.abstraction.ServerConfigManager;
import com.discordsrv.common.config.connection.ConnectionConfig;
import com.discordsrv.common.config.main.MainConfig;
import com.discordsrv.common.config.main.PluginIntegrationConfig;
import com.discordsrv.common.config.main.channels.base.ChannelConfig;
import com.discordsrv.common.config.main.generic.DestinationConfig;
import com.discordsrv.common.config.main.generic.ThreadConfig;
import com.discordsrv.common.console.Console;
import com.discordsrv.common.debug.data.OnlineMode;
import com.discordsrv.common.debug.data.VersionInfo;
import com.discordsrv.common.logging.Logger;
import com.discordsrv.common.logging.backend.impl.JavaLoggerImpl;
import com.discordsrv.common.messageforwarding.game.minecrafttodiscord.MinecraftToDiscordChatModule;
import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.player.provider.AbstractPlayerProvider;
import com.discordsrv.common.plugin.PluginManager;
import com.discordsrv.common.scheduler.Scheduler;
import com.discordsrv.common.scheduler.StandardScheduler;
import com.discordsrv.common.storage.impl.MemoryStorage;
import dev.vankka.dependencydownload.classpath.ClasspathAppender;
import org.jetbrains.annotations.NotNull;
@ -42,12 +50,17 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
@SuppressWarnings("ConstantConditions")
public class MockDiscordSRV extends AbstractDiscordSRV<IBootstrap, MainConfig, ConnectionConfig> {
public static final MockDiscordSRV INSTANCE = new MockDiscordSRV();
public boolean configLoaded = false;
public boolean connectionConfigLoaded = false;
public boolean playerProviderSubscribed = false;
private final Scheduler scheduler = new StandardScheduler(this);
private Path path;
@ -83,6 +96,7 @@ public class MockDiscordSRV extends AbstractDiscordSRV<IBootstrap, MainConfig, C
}
});
load();
versionInfo = new VersionInfo("JUnit", "JUnit", "JUnit", "JUnit");
}
@Override
@ -115,7 +129,7 @@ public class MockDiscordSRV extends AbstractDiscordSRV<IBootstrap, MainConfig, C
@Override
public OnlineMode onlineMode() {
return null;
return OnlineMode.ONLINE;
}
@Override
@ -125,12 +139,39 @@ public class MockDiscordSRV extends AbstractDiscordSRV<IBootstrap, MainConfig, C
@Override
public @NotNull AbstractPlayerProvider<?, ?> playerProvider() {
return null;
return new AbstractPlayerProvider<IPlayer, DiscordSRV>(this) {
@Override
public void subscribe() {
playerProviderSubscribed = true;
}
};
}
@Override
public ConnectionConfigManager<ConnectionConfig> connectionConfigManager() {
return null;
return new ConnectionConfigManager<ConnectionConfig>(this) {
@Override
public ConnectionConfig createConfiguration() {
return connectionConfig();
}
@Override
public void load() {
connectionConfigLoaded = true;
}
};
}
@Override
public ConnectionConfig connectionConfig() {
ConnectionConfig config = new ConnectionConfig();
config.bot.token = FullBootExtension.BOT_TOKEN;
config.storage.backend = MemoryStorage.IDENTIFIER;
config.minecraftAuth.allow = false;
config.update.firstPartyNotification = false;
config.update.security.enabled = false;
config.update.github.enabled = false;
return config;
}
@Override
@ -138,18 +179,47 @@ public class MockDiscordSRV extends AbstractDiscordSRV<IBootstrap, MainConfig, C
return new ServerConfigManager<MainConfig>(this) {
@Override
public MainConfig createConfiguration() {
return new MainConfig() {
@Override
public PluginIntegrationConfig integrations() {
return null;
return config();
}
};
@Override
public void load() {
configLoaded = true;
}
};
}
@Override
public void waitForStatus(Status status) throws InterruptedException {
super.waitForStatus(status);
protected void enable() throws Throwable {
super.enable();
registerModule(MinecraftToDiscordChatModule::new);
}
@Override
public MainConfig config() {
MainConfig config = new MainConfig() {
@Override
public PluginIntegrationConfig integrations() {
return null;
}
};
ChannelConfig global = (ChannelConfig) config.channels.get(GameChannel.DEFAULT_NAME);
DestinationConfig destination = global.destination = new DestinationConfig();
long channelId = Long.parseLong(FullBootExtension.TEST_CHANNEL_ID);
List<Long> channelIds = destination.channelIds;
channelIds.clear();
channelIds.add(channelId);
List<ThreadConfig> threadConfigs = destination.threads;
threadConfigs.clear();
ThreadConfig thread = new ThreadConfig();
thread.channelId = channelId;
threadConfigs.add(thread);
return config;
}
}

View File

@ -0,0 +1,17 @@
package com.discordsrv.common.config;
import com.discordsrv.common.FullBootExtension;
import com.discordsrv.common.MockDiscordSRV;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(FullBootExtension.class)
public class ConfigsLoadOnEnableTest {
@Test
public void configsLoaded() {
Assertions.assertTrue(MockDiscordSRV.INSTANCE.configLoaded, "Config loaded");
Assertions.assertTrue(MockDiscordSRV.INSTANCE.connectionConfigLoaded, "Connection config loaded");
}
}

View File

@ -0,0 +1,121 @@
package com.discordsrv.common.messageforwarding.game;
import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage;
import com.discordsrv.api.event.bus.EventBus;
import com.discordsrv.api.event.bus.Subscribe;
import com.discordsrv.api.event.events.message.forward.game.GameChatMessageForwardedEvent;
import com.discordsrv.api.event.events.message.receive.game.GameChatMessageReceiveEvent;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.FullBootExtension;
import com.discordsrv.common.MockDiscordSRV;
import com.discordsrv.common.channel.GlobalChannel;
import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.player.IPlayer;
import com.discordsrv.common.testing.TestHelper;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ExtendWith(FullBootExtension.class)
public class MinecraftToDiscordChatMessageTest {
@Test
public void runTest() throws InterruptedException {
DiscordSRV discordSRV = MockDiscordSRV.INSTANCE;
EventBus bus = discordSRV.eventBus();
String testMessage = UUID.randomUUID().toString();
CompletableFuture<Void> future = new CompletableFuture<>();
Listener listener = new Listener(testMessage, future);
bus.subscribe(listener);
try {
TestHelper.set(future::completeExceptionally);
MockDiscordSRV.INSTANCE.eventBus().publish(
new GameChatMessageReceiveEvent(
null,
new IPlayer() {
@Override
public @NotNull Identity identity() {
return Identity.identity(UUID.fromString("6c983d46-0631-48b8-9baf-5e33eb5ffec4"));
}
@Override
public @NotNull Audience audience() {
return Audience.empty();
}
@Override
public DiscordSRV discordSRV() {
return MockDiscordSRV.INSTANCE;
}
@Override
public @NotNull String username() {
return "Vankka";
}
@Override
public @NotNull Component displayName() {
return Component.text("Vankka");
}
@Override
public boolean hasPermission(String permission) {
return true;
}
@Override
public void runCommand(String command) {}
},
ComponentUtil.toAPI(Component.text(testMessage)),
new GlobalChannel(MockDiscordSRV.INSTANCE),
false
));
} finally {
TestHelper.set(null);
}
try {
future.get(40, TimeUnit.SECONDS);
} catch (ExecutionException e) {
Assertions.fail(e.getCause());
} catch (TimeoutException e) {
Assertions.fail("Failed to round trip message in 40 seconds", e);
}
}
public static class Listener {
private final String lookFor;
private final CompletableFuture<Void> success;
public Listener(String lookFor, CompletableFuture<Void> success) {
this.lookFor = lookFor;
this.success = success;
}
@Subscribe
public void onForwarded(GameChatMessageForwardedEvent event) {
for (ReceivedDiscordMessage message : event.getDiscordMessage().getMessages()) {
String content = message.getContent();
if (content != null && content.contains(lookFor)) {
success.complete(null);
}
}
}
}
}

View File

@ -0,0 +1,16 @@
package com.discordsrv.common.player.provider;
import com.discordsrv.common.FullBootExtension;
import com.discordsrv.common.MockDiscordSRV;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(FullBootExtension.class)
public class PlayerProviderBootTest {
@Test
public void subscribed() {
Assertions.assertTrue(MockDiscordSRV.INSTANCE.playerProviderSubscribed, "Player provider subscribed");
}
}