diff --git a/.github/test.sh b/.github/test.sh new file mode 100644 index 0000000..6750695 --- /dev/null +++ b/.github/test.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Setup API Server +git clone https://github.com/CodeMC/API build/tmp/CodeMC-API +chmod +x ./build/tmp/CodeMC-API/.github/test.sh + +# Run API Server +cd build/tmp/CodeMC-API +./.github/test.sh +cd ../../../ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1c27c40..a10e4dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: Build Project on: [push, pull_request, workflow_dispatch] jobs: - build: + setup: runs-on: ubuntu-latest timeout-minutes: 30 @@ -21,5 +21,104 @@ jobs: run: chmod +x ./gradlew - name: Gradle Information run: ./gradlew tasks project dependencies + + build: + needs: setup + runs-on: ubuntu-latest + timeout-minutes: 30 + + name: Gradle Build + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: 'gradle' + - name: Change Permissions + run: chmod +x ./gradlew - name: Gradle Build - run: ./gradlew build \ No newline at end of file + run: ./gradlew build -x test + + test: + needs: setup + runs-on: ubuntu-latest + timeout-minutes: 30 + + name: Gradle Test + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: 'gradle' + - name: Change Permissions + run: chmod +x ./gradlew + - name: Setup Servers + run: bash .github/test.sh + - name: Gradle Test + run: ./gradlew test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Stop Servers + if: success() || failure() + run: | + rm -rf /tmp/admin.password + + docker stop jenkins-rest + docker rm jenkins-rest + + docker stop nexus-rest + docker rm nexus-rest + + docker stop mariadb + docker rm mariadb + + docker network rm codemc + - name: Archive Test Reports + uses: actions/upload-artifact@v4 + with: + name: test-report + path: build/reports/tests/test/ + - name: Collect JaCoCo Report + if: ${{ github.event_name != 'pull_request' }} + id: jacoco_reporter + uses: PavanMudigonda/jacoco-reporter@v5.0 + with: + coverage_results_path: build/jacoco.xml + coverage_report_name: Code Coverage + github_token: ${{ secrets.GITHUB_TOKEN }} + skip_check_run: false + minimum_coverage: 80 + fail_below_threshold: false + publish_only_summary: false + + - name: Print JaCoCo Report + if: ${{ github.event_name != 'pull_request' }} + run: | + echo "| Outcome | Value |" >> $GITHUB_STEP_SUMMARY + echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY + echo "| Code Coverage % | ${{ steps.jacoco_reporter.outputs.coverage_percentage }} |" >> $GITHUB_STEP_SUMMARY + echo "| :heavy_check_mark: Number of Lines Covered | ${{ steps.jacoco_reporter.outputs.covered_lines }} |" >> $GITHUB_STEP_SUMMARY + echo "| :x: Number of Lines Missed | ${{ steps.jacoco_reporter.outputs.missed_lines }} |" >> $GITHUB_STEP_SUMMARY + echo "| Total Number of Lines | ${{ steps.jacoco_reporter.outputs.total_lines }} |" >> $GITHUB_STEP_SUMMARY + + - name: Upload Code Coverage Artifacts (Push) + if: ${{ github.event_name != 'pull_request' }} + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: "*/coverage-results.md" + + - name: Upload Code Coverage Artifacts (Pull Request) + if: ${{ github.event_name == 'pull_request' }} + uses: madrapps/jacoco-report@v1.7.1 + with: + paths: build/jacoco.xml + token: ${{ secrets.GITHUB_TOKEN }} + pass-emoji: ✅ + min-coverage-overall: 80 + min-coverage-changed-files: 85 diff --git a/.gitignore b/.gitignore index 79785c4..25cc2bf 100644 --- a/.gitignore +++ b/.gitignore @@ -158,4 +158,4 @@ build/ .vscode/ # Copied configuration -config.json \ No newline at end of file +/config.json \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5d0eb06..88bda46 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ plugins { id "java" id "application" id "com.github.johnrengelman.shadow" version "8.1.1" + id "jacoco" } group 'io.codemc' @@ -34,6 +35,7 @@ repositories { maven { url = 'https://repo.codemc.io/repository/codemc' } maven { url = 'https://repo.codemc.io/repository/codemc' } maven { url = 'https://m2.chew.pro/releases' } + maven { url = 'https://m2.coly.dev/releases' } } dependencies { @@ -45,12 +47,47 @@ dependencies { implementation group: 'pw.chew', name: 'jda-chewtils-command', version: '2.0' implementation group: 'org.spongepowered', name: 'configurate-gson', version: '4.1.2' - implementation group: 'io.codemc.api', name: 'codemc-api', version: '1.1.1' + implementation group: 'io.codemc.api', name: 'codemc-api', version: '1.2.0' implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.9.0' implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-serialization-json', version: '1.7.3' implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '3.5.1' implementation group: 'org.jetbrains.exposed', name: 'exposed-core', version: '0.56.0' implementation group: 'org.jetbrains.exposed', name: 'exposed-jdbc', version: '0.56.0' + + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.11.3' + testImplementation group: 'dev.coly', name: 'JDATesting', version: '0.7.0' + testImplementation group: 'org.mockito', name: 'mockito-core', version: '5.14.2' +} + +tasks { + clean { + delete "logs" + } + + test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + showStandardStreams = true + } + + finalizedBy jacocoTestReport + } + + jacocoTestReport { + dependsOn test + + reports { + csv.required = false + + xml.required = true + xml.outputLocation = layout.buildDirectory.file("jacoco.xml").get().asFile + + html.required = true + html.outputLocation = layout.buildDirectory.dir("jacocoHtml").get().asFile + } + } } artifacts { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 94113f2..e2847c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/io/codemc/bot/CodeMCBot.java b/src/main/java/io/codemc/bot/CodeMCBot.java index 3d39550..d4891e1 100644 --- a/src/main/java/io/codemc/bot/CodeMCBot.java +++ b/src/main/java/io/codemc/bot/CodeMCBot.java @@ -34,6 +34,8 @@ import net.dv8tion.jda.api.entities.Activity; import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.utils.MemberCachePolicy; + +import org.jetbrains.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,8 +44,11 @@ public class CodeMCBot{ - private final Logger logger = LoggerFactory.getLogger(CodeMCBot.class); - private final ConfigHandler configHandler = new ConfigHandler(); + @VisibleForTesting + Logger logger = LoggerFactory.getLogger(CodeMCBot.class); + + @VisibleForTesting + ConfigHandler configHandler = new ConfigHandler(); public static void main(String[] args){ try{ @@ -53,13 +58,48 @@ public static void main(String[] args){ } } - private void start() throws LoginException{ + @VisibleForTesting + void start() throws LoginException{ + loadConfig(); + validateConfig(); + + String token = configHandler.getString("bot_token"); + long owner = configHandler.getLong("users", "owner"); + long guildId = configHandler.getLong("server"); + + CommandClientBuilder clientBuilder = new CommandClientBuilder().setActivity(null).forceGuildOnly(guildId); + + clientBuilder.setOwnerId(owner); + + List coOwners = configHandler.getLongList("users", "co_owners"); + + if(coOwners != null && !coOwners.isEmpty()){ + logger.info("Adding {} Co-Owner(s) to the bot.", coOwners.size()); + // Annoying, but setCoOwnerIds has no overload with a Collection... + long[] coOwnerIds = new long[coOwners.size()]; + for(int i = 0; i < coOwnerIds.length; i++){ + coOwnerIds[i] = coOwners.get(i); + } + + clientBuilder.setCoOwnerIds(coOwnerIds); + } + + initializeAPI(); + login(clientBuilder, token); + } + + private void loadConfig() { if(!configHandler.loadConfig()){ logger.warn("Unable to load config.json! See previous logs for any errors."); System.exit(1); return; } + logger.info("Loaded config.json"); + } + + @VisibleForTesting + public final void validateConfig() { String token = configHandler.getString("bot_token"); if(token == null || token.isEmpty()){ logger.warn("Received invalid Bot Token!"); @@ -67,37 +107,20 @@ private void start() throws LoginException{ return; } - long owner = configHandler.getLong("users", "owner"); - if(owner == -1L){ + if(configHandler.getLong("users", "owner") == -1L){ logger.warn("Unable to retrieve Owner ID. This value is required!"); System.exit(1); return; } - long guildId = configHandler.getLong("server"); - if(guildId == -1L){ + if(configHandler.getLong("server") == -1L){ logger.warn("Unable to retrieve Server ID. This value is required!"); System.exit(1); - return; - } - - CommandClientBuilder clientBuilder = new CommandClientBuilder().setActivity(null).forceGuildOnly(guildId); - - clientBuilder.setOwnerId(owner); - - List coOwners = configHandler.getLongList("users", "co_owners"); - - if(coOwners != null && !coOwners.isEmpty()){ - logger.info("Adding {} Co-Owner(s) to the bot.", coOwners.size()); - // Annoying, but setCoOwnerIds has no overload with a Collection... - long[] coOwnerIds = new long[coOwners.size()]; - for(int i = 0; i < coOwnerIds.length; i++){ - coOwnerIds[i] = coOwners.get(i); - } - - clientBuilder.setCoOwnerIds(coOwnerIds); } + } + @VisibleForTesting + final void initializeAPI() { logger.info("Initializing API..."); JenkinsConfig jenkins = new JenkinsConfig( configHandler.getString("jenkins", "url"), @@ -124,7 +147,7 @@ private void start() throws LoginException{ boolean jenkinsPing = JenkinsAPI.ping(); if (!jenkinsPing) { - logger.error("Failed to connect to Jenkins at {}!", jenkins.getUrl()); + logger.error("Failed to connect to Jenkins at '{}'!", jenkins.getUrl()); System.exit(1); return; } @@ -132,7 +155,7 @@ private void start() throws LoginException{ boolean nexusPing = NexusAPI.ping(); if (!nexusPing) { - logger.error("Failed to connect to Nexus at {}!", nexus.getUrl()); + logger.error("Failed to connect to Nexus at '{}'!", nexus.getUrl()); System.exit(1); return; } @@ -143,7 +166,9 @@ private void start() throws LoginException{ logger.warn("GitHub API Token is empty! This may cause issues with GitHub API requests."); else logger.info("GitHub API Token set"); - + } + + private void login(CommandClientBuilder clientBuilder, String token) throws LoginException{ logger.info("Adding commands..."); clientBuilder.addSlashCommands( new CmdApplication(this), diff --git a/src/main/java/io/codemc/bot/commands/CmdApplication.java b/src/main/java/io/codemc/bot/commands/CmdApplication.java index 53125b2..8905d76 100644 --- a/src/main/java/io/codemc/bot/commands/CmdApplication.java +++ b/src/main/java/io/codemc/bot/commands/CmdApplication.java @@ -32,6 +32,8 @@ import java.util.List; +import org.jetbrains.annotations.VisibleForTesting; + public class CmdApplication extends BotCommand{ public CmdApplication(CodeMCBot bot){ @@ -54,7 +56,8 @@ public void withModalReply(SlashCommandEvent event){} @Override public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member){} - private static class Accept extends BotCommand{ + @VisibleForTesting + static class Accept extends BotCommand{ public Accept(CodeMCBot bot){ super(bot); @@ -81,7 +84,7 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g } try { - long messageId = -1L; + long messageId; if (message.contains("-")) messageId = Long.parseLong(message.split("-")[1]); else @@ -99,7 +102,8 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g } } - private static class Deny extends BotCommand{ + @VisibleForTesting + static class Deny extends BotCommand{ public Deny(CodeMCBot bot){ super(bot); @@ -127,7 +131,7 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g } try { - long messageId = -1L; + long messageId; if (message.contains("-")) messageId = Long.parseLong(message.split("-")[1]); else diff --git a/src/main/java/io/codemc/bot/commands/CmdCodeMC.java b/src/main/java/io/codemc/bot/commands/CmdCodeMC.java index 895845f..61b366c 100644 --- a/src/main/java/io/codemc/bot/commands/CmdCodeMC.java +++ b/src/main/java/io/codemc/bot/commands/CmdCodeMC.java @@ -41,6 +41,8 @@ import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.requests.ErrorResponse; + +import org.jetbrains.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,7 +53,7 @@ public class CmdCodeMC extends BotCommand { - private static final Logger LOGGER = LoggerFactory.getLogger(CmdCodeMC.class); + static final Logger LOGGER = LoggerFactory.getLogger(CmdCodeMC.class); public CmdCodeMC(CodeMCBot bot) { super(bot); @@ -80,7 +82,8 @@ public void withModalReply(SlashCommandEvent event) { public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member) { } - private static class Jenkins extends BotCommand { + @VisibleForTesting + static class Jenkins extends BotCommand { public Jenkins(CodeMCBot bot) { super(bot); @@ -90,7 +93,6 @@ public Jenkins(CodeMCBot bot) { this.options = List.of( new OptionData(OptionType.STRING, "job", "The Jenkins Job Location to fetch. I.e. \"CodeMC/API\".").setRequired(true) - ); } @@ -106,15 +108,23 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g return; } + MessageEmbed embed = createInfoEmbed(job); + if (embed == null) { + CommandUtil.EmbedReply.from(hook).error("Failed to fetch Jenkins Job Info!").send(); + return; + } + + hook.editOriginalEmbeds(embed).queue(); + } + + @VisibleForTesting + MessageEmbed createInfoEmbed(String job) { String jenkinsUrl = bot.getConfigHandler().getString("jenkins", "url"); String username = job.split("/")[0]; String jobName = job.split("/")[1]; JenkinsJob info = JenkinsAPI.getJobInfo(username, jobName); - if (info == null) { - CommandUtil.EmbedReply.from(hook).error("Failed to fetch Jenkins Job Info!").send(); - return; - } + if (info == null) return null; EmbedBuilder embed = CommandUtil.getEmbed() .setTitle(job, info.getUrl()) @@ -133,12 +143,13 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g if (info.getLastStableBuild() != null) embed.addField("Last Stable Build", info.getLastStableBuild().toString(), false); - - hook.editOriginalEmbeds(embed.build()).queue(); + + return embed.build(); } } - private static class Nexus extends BotCommand { + @VisibleForTesting + static class Nexus extends BotCommand { public Nexus(CodeMCBot bot) { super(bot); @@ -164,14 +175,22 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g return; } + MessageEmbed embed = createInfoEmbed(user); + if (embed == null) { + CommandUtil.EmbedReply.from(hook).error("Failed to fetch Nexus Repository Info!").send(); + return; + } + + hook.sendMessageEmbeds(embed).queue(); + } + + @VisibleForTesting + MessageEmbed createInfoEmbed(String user) { String nexusUrl = bot.getConfigHandler().getString("nexus", "url"); String repository = user.toLowerCase(); JsonObject info = NexusAPI.getNexusRepository(repository); - if (info == null) { - CommandUtil.EmbedReply.from(hook).error("Failed to fetch Nexus Repository Info!").send(); - return; - } + if (info == null) return null; String format = ((JsonPrimitive) info.get("format")).getContent(); String type = ((JsonPrimitive) info.get("type")).getContent(); @@ -183,12 +202,13 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g .addField("Type", type, true) .setTimestamp(Instant.now()) .build(); - - hook.sendMessageEmbeds(embed).queue(); + + return embed; } } - private static class Remove extends BotCommand { + @VisibleForTesting + static class Remove extends BotCommand { public Remove(CodeMCBot bot) { super(bot); @@ -216,7 +236,7 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g return; } - if (JenkinsAPI.getJenkinsUser(username).isBlank()) { + if (!JenkinsAPI.existsUser(username)) { CommandUtil.EmbedReply.from(hook).error("The user does not have a Jenkins account!").send(); return; } @@ -234,6 +254,10 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g long id = dbUser.getDiscord(); Member user = guild.getMemberById(id); + if (user == null) { + CommandUtil.EmbedReply.from(hook).success("Successfully removed " + username + " from the CodeMC Services!").send(); + return; + } Role authorRole = guild.getRoleById(bot.getConfigHandler().getLong("author_role")); if (authorRole == null) { @@ -245,7 +269,7 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g .anyMatch(role -> role.getIdLong() == authorRole.getIdLong()); if (!hasAuthor) { - CommandUtil.EmbedReply.from(hook).error("The user is not an Author!").send(); + CommandUtil.EmbedReply.from(hook).error("User was deleted, but is not an Author!").send(); return; } @@ -266,7 +290,8 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g } } - private static class Validate extends BotCommand{ + @VisibleForTesting + static class Validate extends BotCommand{ public Validate(CodeMCBot bot) { super(bot); @@ -340,7 +365,8 @@ private boolean validate(InteractionHook hook, String username, AtomicInteger co } } - private static class Link extends BotCommand{ + @VisibleForTesting + static class Link extends BotCommand{ public Link(CodeMCBot bot) { super(bot); @@ -364,7 +390,12 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g String username = event.getOption("username", null, OptionMapping::getAsString); Member target = event.getOption("discord", null, OptionMapping::getAsMember); - if (JenkinsAPI.getJenkinsUser(username).isBlank()) { + if (username == null || username.isEmpty()) { + CommandUtil.EmbedReply.from(hook).error("Invalid Jenkins User provided!").send(); + return; + } + + if (!JenkinsAPI.existsUser(username)) { CommandUtil.EmbedReply.from(hook).error("The user does not have a Jenkins account!").send(); return; } @@ -396,7 +427,8 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g } } - private static class Unlink extends BotCommand { + @VisibleForTesting + static class Unlink extends BotCommand { public Unlink(CodeMCBot bot) { super(bot); @@ -431,6 +463,14 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g .filter(user -> userTarget == null || user.equals(userTarget)) .findFirst() .orElse(null); + + if (username == null) { + if (userTarget == null) + CommandUtil.EmbedReply.from(hook).error("The user is not linked to any Jenkins/Nexus account!").send(); + else + CommandUtil.EmbedReply.from(hook).error("The user is not linked to the specified Jenkins/Nexus account!").send(); + return; + } if (DatabaseAPI.getUser(username) == null) { CommandUtil.EmbedReply.from(hook).error("The user is not linked to any Jenkins/Nexus account!").send(); @@ -443,7 +483,8 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g } } - private static class ChangePassword extends BotCommand { + @VisibleForTesting + static class ChangePassword extends BotCommand { public ChangePassword(CodeMCBot bot) { super(bot); @@ -490,14 +531,17 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g return; } - if (JenkinsAPI.getJenkinsUser(username).isBlank()) { - CommandUtil.EmbedReply.from(hook).error("This user does not have a Jenkins account!").send(); + if (!JenkinsAPI.existsUser(username)) { + CommandUtil.EmbedReply.from(hook).error("You do not have a Jenkins account!").send(); return; } String password = APIUtil.newPassword(); boolean success = APIUtil.changePassword(hook, username, password); - if (!success) return; + if (!success) { + CommandUtil.EmbedReply.from(hook).error("Failed to regenerate your Nexus Credentials!").send(); + return; + } CommandUtil.EmbedReply.from(hook) .success("Successfully changed your password!") @@ -505,7 +549,8 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g } } - private static class CreateUser extends BotCommand{ + @VisibleForTesting + static class CreateUser extends BotCommand{ public CreateUser(CodeMCBot bot) { super(bot); @@ -529,7 +574,12 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g String username = event.getOption("username", null, OptionMapping::getAsString); Member target = event.getOption("discord", null, OptionMapping::getAsMember); - if (!JenkinsAPI.getJenkinsUser(username).isBlank()) { + if (username == null || username.isEmpty()) { + CommandUtil.EmbedReply.from(hook).error("Invalid Username provided!").send(); + return; + } + + if (JenkinsAPI.existsUser(username)) { CommandUtil.EmbedReply.from(hook).error("A user with that username already exists.").send(); return; } @@ -569,11 +619,12 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g APIUtil.createNexus(hook, username, password); CommandUtil.EmbedReply.from(hook).success("Successfully created user " + username + " and linked it to " + target.getUser().getEffectiveName() + "!").send(); - LOGGER.info("Created user '" + username + "' in the Jenkins/Nexus services."); + LOGGER.info("Created user '{}' in the Jenkins/Nexus services.", username); } } - private static class DeleteUser extends BotCommand{ + @VisibleForTesting + static class DeleteUser extends BotCommand{ public DeleteUser(CodeMCBot bot) { super(bot); @@ -595,8 +646,13 @@ public void withModalReply(SlashCommandEvent event) {} public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member) { String username = event.getOption("username", null, OptionMapping::getAsString); - if (!JenkinsAPI.getJenkinsUser(username).isBlank()) { - CommandUtil.EmbedReply.from(hook).error("A user with that username already exists.").send(); + if (username == null || username.isEmpty()) { + CommandUtil.EmbedReply.from(hook).error("Invalid Username provided!").send(); + return; + } + + if (!JenkinsAPI.existsUser(username)) { + CommandUtil.EmbedReply.from(hook).error("The user does not exist!").send(); return; } @@ -605,7 +661,7 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g NexusAPI.deleteNexus(username); CommandUtil.EmbedReply.from(hook).success("Successfully deleted user " + username + "!").send(); - LOGGER.info("Deleted user '" + username + "' from the Jenkins/Nexus services."); + LOGGER.info("Deleted user '{}' from the Jenkins/Nexus services.", username); } } } diff --git a/src/main/java/io/codemc/bot/commands/CmdMsg.java b/src/main/java/io/codemc/bot/commands/CmdMsg.java index e99e35c..6878ce5 100644 --- a/src/main/java/io/codemc/bot/commands/CmdMsg.java +++ b/src/main/java/io/codemc/bot/commands/CmdMsg.java @@ -25,7 +25,6 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.interactions.InteractionHook; @@ -39,6 +38,8 @@ import java.util.Arrays; +import org.jetbrains.annotations.VisibleForTesting; + public class CmdMsg extends BotCommand{ public CmdMsg(CodeMCBot bot){ @@ -61,7 +62,8 @@ public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild g @Override public void withModalReply(SlashCommandEvent event){} - private static class Post extends BotCommand{ + @VisibleForTesting + static class Post extends BotCommand{ public Post(CodeMCBot bot){ super(bot); @@ -96,7 +98,7 @@ public void withModalReply(SlashCommandEvent event){ } TextInput input = TextInput.create("message", "Message", TextInputStyle.PARAGRAPH) - .setMaxLength(asEmbed ? MessageEmbed.DESCRIPTION_MAX_LENGTH : Message.MAX_CONTENT_LENGTH) + .setMaxLength(asEmbed ? TextInput.MAX_VALUE_LENGTH : Message.MAX_CONTENT_LENGTH) .setRequired(true) .build(); @@ -108,7 +110,8 @@ public void withModalReply(SlashCommandEvent event){ } } - private static class Edit extends BotCommand{ + @VisibleForTesting + static class Edit extends BotCommand{ public Edit(CodeMCBot bot){ super(bot); diff --git a/src/main/java/io/codemc/bot/config/ConfigHandler.java b/src/main/java/io/codemc/bot/config/ConfigHandler.java index 14aec2f..c0d18bb 100644 --- a/src/main/java/io/codemc/bot/config/ConfigHandler.java +++ b/src/main/java/io/codemc/bot/config/ConfigHandler.java @@ -36,12 +36,14 @@ public class ConfigHandler{ private final Logger logger = LoggerFactory.getLogger(ConfigHandler.class); private final File file = new File("./config.json"); + private boolean loaded = false; private ConfigurationNode node = null; public ConfigHandler(){} public boolean loadConfig(){ + if (loaded) return reloadConfig(); logger.info("Loading config.json..."); if(!file.exists()){ @@ -59,6 +61,7 @@ public boolean loadConfig(){ } } + loaded = true; return reloadConfig(); } @@ -102,4 +105,12 @@ public List getStringList(Object... path){ return Collections.emptyList(); } } + + public void set(Object value, Object... path){ + try{ + node.node(path).set(value); + }catch(SerializationException ex){ + logger.error("Unable to set value in Configuration!", ex); + } + } } diff --git a/src/main/java/io/codemc/bot/listeners/ButtonListener.java b/src/main/java/io/codemc/bot/listeners/ButtonListener.java index 966082e..f7bb33e 100644 --- a/src/main/java/io/codemc/bot/listeners/ButtonListener.java +++ b/src/main/java/io/codemc/bot/listeners/ButtonListener.java @@ -32,12 +32,16 @@ import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; import net.dv8tion.jda.api.interactions.modals.Modal; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; public class ButtonListener extends ListenerAdapter{ private final CodeMCBot bot; + private final Logger logger = LoggerFactory.getLogger(ButtonListener.class); public ButtonListener(CodeMCBot bot){ this.bot = bot; @@ -50,9 +54,11 @@ public void onButtonInteraction(@NotNull ButtonInteractionEvent event){ CommandUtil.EmbedReply.from(event).error("Buttons only work on the CodeMC Server!").send(); return; } - - if(event.getButton().getId() == null){ - event.deferReply().queue(); + + String id = event.getButton().getId(); + if (id == null) { + CommandUtil.EmbedReply.from(event).error("Received Button Interaction with no ID!").send(); + logger.error("Received Button Interaction with no ID!"); return; } @@ -61,6 +67,7 @@ public void onButtonInteraction(@NotNull ButtonInteractionEvent event){ if(acceptApplicationRoles.isEmpty() || denyApplicationRoles.isEmpty()){ CommandUtil.EmbedReply.from(event).error("No roles for accepting or denying applications set!").send(); + logger.error("No roles for accepting or denying applications set!"); return; } @@ -70,7 +77,7 @@ public void onButtonInteraction(@NotNull ButtonInteractionEvent event){ return; } - String[] values = event.getButton().getId().split(":"); + String[] values = id.split(":"); if(values.length < 4 || !values[0].equals("application")){ CommandUtil.EmbedReply.from(event).error("Received non-application button event!").send(); return; @@ -79,7 +86,7 @@ public void onButtonInteraction(@NotNull ButtonInteractionEvent event){ if(!values[1].equals("accept") && !values[1].equals("deny")){ CommandUtil.EmbedReply.from(event).error( "Received unknown Button Application type.", - "Expected `accept` or `deny` but got " + values[1] + "." + "Expected `accept` or `deny` but got `" + values[1] + "`." ).send(); return; } @@ -117,7 +124,8 @@ public void onButtonInteraction(@NotNull ButtonInteractionEvent event){ } } - private boolean lacksRole(List roleIds, List allowedRoleIds){ + @VisibleForTesting + boolean lacksRole(List roleIds, List allowedRoleIds){ if(roleIds.isEmpty()) return true; diff --git a/src/main/java/io/codemc/bot/listeners/ModalListener.java b/src/main/java/io/codemc/bot/listeners/ModalListener.java index c64b3e7..c2dbc96 100644 --- a/src/main/java/io/codemc/bot/listeners/ModalListener.java +++ b/src/main/java/io/codemc/bot/listeners/ModalListener.java @@ -32,14 +32,11 @@ import net.dv8tion.jda.api.interactions.InteractionHook; import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.interactions.modals.ModalMapping; -import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.utils.MarkdownUtil; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.time.Instant; - public class ModalListener extends ListenerAdapter{ private final Logger logger = LoggerFactory.getLogger(ModalListener.class); @@ -72,7 +69,7 @@ public void onModalInteraction(@NotNull ModalInteractionEvent event){ return; } - if (!JenkinsAPI.getJenkinsUser(user).isEmpty()) { + if (JenkinsAPI.existsUser(user)) { CommandUtil.EmbedReply.from(hook) .error("A Jenkins User named '" + user + "' already exists!") .send(); @@ -103,14 +100,7 @@ public void onModalInteraction(@NotNull ModalInteractionEvent event){ String repoLink = MarkdownUtil.maskedLink(repo, repoLinkValue); String submitter = String.format("`%s` (%s)", event.getUser().getEffectiveName(), event.getUser().getAsMention()); - MessageEmbed embed = CommandUtil.getEmbed() - .addField("User/Organisation:", userLink, true) - .addField("Repository:", repoLink, true) - .addField("Submitted by:", submitter, true) - .addField("Description", description, false) - .setFooter(event.getUser().getId()) - .setTimestamp(Instant.now()) - .build(); + MessageEmbed embed = CommandUtil.requestEmbed(userLink, repoLink, submitter, description, event.getUser().getId()); requestChannel.sendMessageEmbeds(embed) .setActionRow( @@ -122,18 +112,20 @@ public void onModalInteraction(@NotNull ModalInteractionEvent event){ "[Request sent!](" + message.getJumpUrl() + ")") .send(); - RestAction.allOf( - message.createThreadChannel("Access Request - " + event.getUser().getName()), - message.addReaction(Emoji.fromCustom("like", 935126958193405962L, false)), - message.addReaction(Emoji.fromCustom("dislike", 935126958235344927L, false)) - ).queue(); + message.createThreadChannel("Access Request - " + event.getUser().getName()).queue(); + message.addReaction(Emoji.fromCustom("like", 935126958193405962L, false)).queue(); + message.addReaction(Emoji.fromCustom("dislike", 935126958235344927L, false)).queue(); logger.info("[Access Request] User {} requested access to the CI.", event.getUser().getEffectiveName()); }, - e -> CommandUtil.EmbedReply.from(hook).error( - "Error while submitting request!", - "Reported Error: " + e.getMessage() - ).send() + e -> { + CommandUtil.EmbedReply.from(hook).error( + "Error while submitting request!", + "Reported Error: " + e.getMessage() + ).send(); + + logger.error("Error while submitting request", e); + } ); }); @@ -209,16 +201,24 @@ public void onModalInteraction(@NotNull ModalInteractionEvent event){ if(asEmbed){ message.editMessageEmbeds(CommandUtil.getEmbed().setDescription(text).build()).setReplace(true).queue( m -> sendConfirmation(hook, m, true), - e -> CommandUtil.EmbedReply.from(hook) - .error("Unable to edit message. Reason: " + e.getMessage()) - .send() + e -> { + CommandUtil.EmbedReply.from(hook) + .error("Unable to edit message. Reason: " + e.getMessage()) + .send(); + + logger.error("Error while editing message", e); + } ); }else{ message.editMessage(text).setReplace(true).queue( m -> sendConfirmation(hook, m, true), - e -> CommandUtil.EmbedReply.from(hook) - .error("Unable to edit message. Reason: " + e.getMessage()) - .send() + e -> { + CommandUtil.EmbedReply.from(hook) + .error("Unable to edit message. Reason: " + e.getMessage()) + .send(); + + logger.error("Error while editing message", e); + } ); } } diff --git a/src/main/java/io/codemc/bot/utils/APIUtil.java b/src/main/java/io/codemc/bot/utils/APIUtil.java index 38ed493..446f4ab 100644 --- a/src/main/java/io/codemc/bot/utils/APIUtil.java +++ b/src/main/java/io/codemc/bot/utils/APIUtil.java @@ -43,11 +43,12 @@ public static boolean createNexus(InteractionHook hook, String username, String return true; } - public static boolean createJenkinsJob(InteractionHook hook, String username, String password, String project, String repoLink) { - if (!JenkinsAPI.getJenkinsUser(username).isEmpty()) { - CommandUtil.EmbedReply.from(hook) - .error("Jenkins User for " + username + " already exists!") - .send(); + public static boolean createJenkinsJob(InteractionHook hook, String username, String password, String project, String repoLink, boolean trigger) { + if (JenkinsAPI.existsUser(username)) { + if (hook != null) + CommandUtil.EmbedReply.from(hook) + .error("Jenkins User for " + username + " already exists!") + .send(); LOGGER.error("Jenkins User for {} already exists!", username); return false; @@ -55,9 +56,10 @@ public static boolean createJenkinsJob(InteractionHook hook, String username, St boolean userSuccess = JenkinsAPI.createJenkinsUser(username, password, isGroup(username)); if (!userSuccess) { - CommandUtil.EmbedReply.from(hook) - .error("Failed to create Jenkins User for " + username + "!") - .send(); + if (hook != null) + CommandUtil.EmbedReply.from(hook) + .error("Failed to create Jenkins User for " + username + "!") + .send(); LOGGER.error("Failed to create Jenkins User for {}!", username); return false; @@ -66,22 +68,26 @@ public static boolean createJenkinsJob(InteractionHook hook, String username, St boolean freestyle = JenkinsAPI.isFreestyle(repoLink); boolean jobSuccess = JenkinsAPI.createJenkinsJob(username, project, repoLink, freestyle); if (!jobSuccess) { - CommandUtil.EmbedReply.from(hook) - .error("Failed to create Jenkins Job '" + project + "' for " + username + "!") - .send(); + if (hook != null) + CommandUtil.EmbedReply.from(hook) + .error("Failed to create Jenkins Job '" + project + "' for " + username + "!") + .send(); LOGGER.error("Failed to create Jenkins Job '{}' for {}!", project, username); return false; } - boolean triggerBuild = JenkinsAPI.triggerBuild(username, project); - if (!triggerBuild) { - CommandUtil.EmbedReply.from(hook) - .error("Failed to trigger Jenkins Build for " + username + "!") - .send(); - - LOGGER.error("Failed to trigger Jenkins Build for {}!", username); - return false; + if (trigger) { + boolean triggerBuild = JenkinsAPI.triggerBuild(username, project); + if (!triggerBuild) { + if (hook != null) + CommandUtil.EmbedReply.from(hook) + .error("Failed to trigger Jenkins Build for " + username + "!") + .send(); + + LOGGER.error("Failed to trigger Jenkins Build for {}!", username); + return false; + } } LOGGER.info("Successfully created Jenkins Job '{}' for {}!", project, username); diff --git a/src/main/java/io/codemc/bot/utils/ApplicationHandler.java b/src/main/java/io/codemc/bot/utils/ApplicationHandler.java index 8d3908d..1c1c707 100644 --- a/src/main/java/io/codemc/bot/utils/ApplicationHandler.java +++ b/src/main/java/io/codemc/bot/utils/ApplicationHandler.java @@ -29,12 +29,12 @@ import net.dv8tion.jda.api.requests.ErrorResponse; import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; import net.dv8tion.jda.api.utils.messages.MessageCreateData; + +import org.jetbrains.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class ApplicationHandler{ @@ -65,7 +65,7 @@ public static void handle(CodeMCBot bot, InteractionHook hook, Guild guild, long CommandUtil.EmbedReply.from(hook).error("Provided Message does not have any embeds.").send(); return; } - + MessageEmbed embed = embeds.get(0); if(embed.getFooter() == null || embed.getFields().isEmpty()){ CommandUtil.EmbedReply.from(hook).error("Embed does not have a Footer or any Embed Fields").send(); @@ -87,7 +87,7 @@ public static void handle(CodeMCBot bot, InteractionHook hook, Guild guild, long CommandUtil.EmbedReply.from(hook).error("Embed does not have a valid footer.").send(); return; } - + hook.editOriginalFormat( """ [2/5] Handling Join Request... @@ -114,7 +114,7 @@ public static void handle(CodeMCBot bot, InteractionHook hook, Guild guild, long String user = userField.getValue(); String repo = repoField.getValue(); - String username = user.substring(1, user.indexOf("]"));; + String username = user.substring(1, user.indexOf("]")); String userLink = user.substring(user.indexOf("(") + 1, user.length() - 1); String repoName = repo.substring(1, repo.indexOf("]")); String repoLink = repo.substring(repo.indexOf("(") + 1, repo.length() - 1); @@ -166,10 +166,10 @@ public static void handle(CodeMCBot bot, InteractionHook hook, Guild guild, long if(accepted){ String password = APIUtil.newPassword(); - boolean jenkinsSuccess = APIUtil.createJenkinsJob(hook, username, password, repoName, repoLink); + boolean jenkinsSuccess = APIUtil.createJenkinsJob(hook, username, password, repoName, repoLink, true); boolean nexusSuccess = APIUtil.createNexus(hook, username, password); - if(!jenkinsSuccess || ! nexusSuccess) + if(!jenkinsSuccess || !nexusSuccess) return; if(member == null){ @@ -310,7 +310,8 @@ public static void handle(CodeMCBot bot, InteractionHook hook, Guild guild, long }); } - private static MessageCreateData getMessage(CodeMCBot bot, String userId, String userLink, String repoLink, String str, User reviewer, boolean accepted){ + @VisibleForTesting + static MessageCreateData getMessage(CodeMCBot bot, String userId, String userLink, String repoLink, String str, User reviewer, boolean accepted){ String msg = String.join("\n", bot.getConfigHandler().getStringList("messages", (accepted ? "accepted" : "denied"))); MessageEmbed embed = new EmbedBuilder() diff --git a/src/main/java/io/codemc/bot/utils/CommandUtil.java b/src/main/java/io/codemc/bot/utils/CommandUtil.java index 55aa5b5..1e97611 100644 --- a/src/main/java/io/codemc/bot/utils/CommandUtil.java +++ b/src/main/java/io/codemc/bot/utils/CommandUtil.java @@ -22,19 +22,19 @@ import com.jagrosh.jdautilities.command.SlashCommandEvent; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.interactions.InteractionHook; import org.slf4j.LoggerFactory; +import java.time.Instant; import java.util.List; public class CommandUtil{ private static final Logger LOG = (Logger)LoggerFactory.getLogger(CommandUtil.class); - - public CommandUtil(){} - + public static EmbedBuilder getEmbed(){ return new EmbedBuilder().setColor(0x0172BA); } @@ -49,6 +49,25 @@ public static boolean hasRole(Member member, List roleIds){ .findFirst() .orElse(null) != null; } + + public static MessageEmbed embedError(String... lines){ + return EmbedReply.empty().error(lines).build(); + } + + public static MessageEmbed embedSuccess(String... lines){ + return EmbedReply.empty().success(lines).build(); + } + + public static MessageEmbed requestEmbed(String userLink, String repoLink, String submitter, String description, String id) { + return getEmbed() + .addField("User/Organisation:", userLink, true) + .addField("Repository:", repoLink, true) + .addField("Submitted by:", submitter, true) + .addField("Description", description, false) + .setFooter(id) + .setTimestamp(Instant.now()) + .build(); + } public static class EmbedReply { @@ -62,6 +81,10 @@ private EmbedReply(T type){ public static EmbedReply from(T type){ return new EmbedReply<>(type); } + + public static EmbedReply empty() { + return new EmbedReply<>(null); + } public EmbedReply success(String... lines){ builder.setDescription(String.join("\n", lines)) @@ -83,19 +106,25 @@ public EmbedReply error(String... lines){ .setColor(0xFF0000); return this; } + + public MessageEmbed build() { + return builder.build(); + } public void send(){ + if(type == null) return; + if(type instanceof SlashCommandEvent commandEvent){ - commandEvent.replyEmbeds(builder.build()).setEphemeral(true).queue(); + commandEvent.replyEmbeds(build()).setEphemeral(true).queue(); }else if(type instanceof ModalInteractionEvent modalEvent){ - modalEvent.replyEmbeds(builder.build()).setEphemeral(true).queue(); + modalEvent.replyEmbeds(build()).setEphemeral(true).queue(); }else if(type instanceof ButtonInteractionEvent buttonEvent){ - buttonEvent.replyEmbeds(builder.build()).setEphemeral(true).queue(); + buttonEvent.replyEmbeds(build()).setEphemeral(true).queue(); }else if(type instanceof InteractionHook hook){ - hook.editOriginal(EmbedBuilder.ZERO_WIDTH_SPACE).setEmbeds(builder.build()).queue(); + hook.editOriginal(EmbedBuilder.ZERO_WIDTH_SPACE).setEmbeds(build()).queue(); }else{ LOG.error("Received unknown Type {} for EmbedReply!", type); } diff --git a/src/test/java/io/codemc/bot/MockCodeMCBot.java b/src/test/java/io/codemc/bot/MockCodeMCBot.java new file mode 100644 index 0000000..c1ad86e --- /dev/null +++ b/src/test/java/io/codemc/bot/MockCodeMCBot.java @@ -0,0 +1,71 @@ +package io.codemc.bot; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.slf4j.LoggerFactory; + +import io.codemc.api.jenkins.JenkinsAPI; +import io.codemc.api.nexus.NexusAPI; +import io.codemc.bot.config.ConfigHandler; +import io.codemc.bot.utils.APIUtil; + +public class MockCodeMCBot extends CodeMCBot { + + public static final MockCodeMCBot INSTANCE = new MockCodeMCBot(); + + private MockCodeMCBot() { + logger = LoggerFactory.getLogger(MockCodeMCBot.class); + configHandler = new ConfigHandler(); + configHandler.loadConfig(); + + setTestConfig(); + start(); + } + + public void setTestConfig() { + // Set Nexus Password + try { + File file = new File("/tmp/admin.password"); + if (file.exists()) { + String password = Files.readString(file.toPath()); + configHandler.set(password, "nexus", "password"); + } else { + logger.warn("Failed to read Nexus password from file: File does not exist"); + } + } catch (IOException e) { + logger.error("Failed to read Nexus password from file", e); + } + + // Set GitHub Token + String token = System.getenv("GITHUB_TOKEN"); + if (token != null && !token.isEmpty()) { + configHandler.set(System.getenv("GITHUB_TOKEN"), "github"); + } + } + + @Override + void start() { + logger.info("Starting test bot..."); + + validateConfig(); + initializeAPI(); + } + + public void create(String username, String job) { + String link = "https://github.com/" + username + "/" + job; + if (!JenkinsAPI.existsUser(username)) { + String password = APIUtil.newPassword(); + APIUtil.createJenkinsJob(null, username, password, job, link, false); + APIUtil.createNexus(null, username, password); + } else { + JenkinsAPI.createJenkinsJob(username, job, link, JenkinsAPI.isFreestyle(link)); + } + } + + public void delete(String username) { + JenkinsAPI.deleteUser(username); + NexusAPI.deleteNexus(username); + } +} diff --git a/src/test/java/io/codemc/bot/MockJDA.java b/src/test/java/io/codemc/bot/MockJDA.java new file mode 100644 index 0000000..c9fab2a --- /dev/null +++ b/src/test/java/io/codemc/bot/MockJDA.java @@ -0,0 +1,798 @@ +package io.codemc.bot; + +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import dev.coly.jdat.JDAObjects; +import dev.coly.util.Callback; +import io.codemc.bot.commands.BotCommand; +import io.codemc.bot.commands.TestCommandListener; +import io.codemc.bot.config.ConfigHandler; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.Message.Attachment; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; +import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.interactions.Interaction; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.InteractionType; +import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.components.ItemComponent; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.modals.Modal; +import net.dv8tion.jda.api.interactions.modals.ModalMapping; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.requests.restaction.*; +import net.dv8tion.jda.api.requests.restaction.interactions.ModalCallbackAction; +import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; +import net.dv8tion.jda.api.utils.messages.MessageRequest; +import org.junit.jupiter.api.Assertions; + +import java.security.SecureRandom; +import java.util.*; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@SuppressWarnings("unchecked") +public class MockJDA { + + private static final ConfigHandler CONFIG = MockCodeMCBot.INSTANCE.getConfigHandler(); + private static final SecureRandom RANDOM = new SecureRandom(); + + private static final Map messages = new HashMap<>(); + private static final Map embeds = new HashMap<>(); + private static final Map members = new HashMap<>(); + private static final Map latestMessages = new HashMap<>(); + private static final Map latestEmbeds = new HashMap<>(); + + public static final JDA JDA = JDAObjects.getJDA(); + public static final Member SELF = mockMember("Bot"); + public static final Guild GUILD = mockGuild(); + public static Modal CURRENT_MODAL = null; + + public static final TextChannel GENERAL = mockChannel("general"); // Not found, let it be -1 + public static final TextChannel REQUEST_CHANNEL = mockChannel("request_access"); + public static final TextChannel ACCEPTED_CHANNEL = mockChannel("accepted_requests"); + public static final TextChannel REJECTED_CHANNEL = mockChannel("rejected_requests"); + public static final List CHANNELS = List.of(GENERAL, REQUEST_CHANNEL, ACCEPTED_CHANNEL, REJECTED_CHANNEL); + + public static final Role ADMINISTRATOR = mockRole("Administrator", 405917902865170453L, 4); + public static final Role MAINTAINER = mockRole("Maintainer", 659568973079379971L, 3); + public static final Role REVIEWER = mockRole("Reviewer", 1233971297185431582L, 2); + public static final Role AUTHOR = mockRole("Author", CONFIG.getLong("author_role"), 1); + public static final List ROLES = List.of(ADMINISTRATOR, MAINTAINER, REVIEWER, AUTHOR); + + public static String getMessage(long id) { + return messages.get(id); + } + + public static List getEmbeds(long id) { + return Arrays.asList(embeds.get(id)); + } + + public static String getLatestMessage(MessageChannel channel) { + return latestMessages.get(channel.getIdLong()); + } + + public static List getLatestEmbeds(MessageChannel channel) { + return Arrays.asList(latestEmbeds.get(channel.getIdLong())); + } + + public static InteractionHook mockInteractionHook(Member user, MessageChannel channel, InteractionType type) { + return mockInteractionHook(user, channel, type, RANDOM.nextLong(0, Long.MAX_VALUE)); + } + + public static InteractionHook mockInteractionHook(Member user, MessageChannel channel, InteractionType type, long id) { + return mockInteractionHook(mockInteraction(user, channel, type, id)); + } + + public static InteractionHook mockInteractionHook(Interaction interaction) { + InteractionHook hook = mock(InteractionHook.class); + when(hook.getJDA()).thenReturn(JDA); + when(hook.getExpirationTimestamp()).thenReturn(0L); + when(hook.getIdLong()).thenAnswer(inv -> interaction.getIdLong()); + + when(hook.getInteraction()).thenReturn(interaction); + + MessageChannel channel = interaction.getMessageChannel(); + + when(hook.editOriginal(anyString())).thenAnswer(inv -> { + String content = inv.getArgument(0); + messages.put(hook.getIdLong(), content); + return mockWebhookReply(WebhookMessageEditAction.class, hook, mockMessage(content, channel, hook.getIdLong())); + }); + when(hook.editOriginalFormat(anyString(), any(Object[].class))).thenAnswer(inv -> { + String content = String.format(inv.getArgument(0), (Object[]) inv.getRawArguments()[1]); + messages.put(hook.getIdLong(), content); + return mockWebhookReply(WebhookMessageEditAction.class, hook, mockMessage(content, channel, hook.getIdLong())); + }); + + when(hook.editOriginalEmbeds(any(MessageEmbed[].class))).thenAnswer(inv -> { + Object obj = inv.getArgument(0); + if (obj instanceof MessageEmbed[] allEmbeds) + embeds.put(hook.getIdLong(), allEmbeds); + else + embeds.put(hook.getIdLong(), new MessageEmbed[] { (MessageEmbed) obj }); + + return mockWebhookReply(WebhookMessageEditAction.class, hook, mockMessage(null, Arrays.asList(embeds.get(hook.getIdLong())), channel, hook.getIdLong())); + }); + + when(hook.sendMessageEmbeds(any(), any(MessageEmbed[].class))).thenAnswer(inv -> { + MessageEmbed first = inv.getArgument(0); + + List embeds = new ArrayList<>(); + embeds.add(first); + if (inv.getArguments().length > 1) { + Object obj = inv.getArgument(1); + + if (obj instanceof MessageEmbed[] allEmbeds) + embeds.addAll(Arrays.asList(allEmbeds)); + else + embeds.add((MessageEmbed) obj); + } + + return mockWebhookReply(WebhookMessageCreateAction.class, hook, mockMessage(null, embeds, channel, hook.getIdLong())); + }); + + return hook; + } + + public static Interaction mockInteraction(Member user, MessageChannel channel, InteractionType type, long id) { + Interaction interaction = mock(Interaction.class); + + when(interaction.getJDA()).thenReturn(JDA); + when(interaction.getChannel()).thenReturn(channel); + when(interaction.getMessageChannel()).thenReturn(channel); + + when(interaction.getMember()).thenReturn(user); + when(interaction.getUser()).thenAnswer(inv -> user.getUser()); + when(interaction.getGuild()).thenReturn(GUILD); + when(interaction.getTypeRaw()).thenReturn(type.getKey()); + when(interaction.getIdLong()).thenReturn(id); + + return interaction; + } + + private static MessageChannelUnion mockUnion(MessageChannel channel) { + MessageChannelUnion union = mock(MessageChannelUnion.class); + when(union.asTextChannel()).thenAnswer(inv -> channel); + when(union.getJDA()).thenReturn(JDA); + + return union; + } + + public static TextChannel mockChannel(String configName) { + long id = CONFIG.getLong("channels", configName); + TextChannel channel = (TextChannel) JDAObjects.getMessageChannel(configName.replace('_', '-'), id, Callback.single()); + + when(channel.getGuild()).thenReturn(GUILD); + when(channel.getJDA()).thenReturn(JDA); + when(channel.canTalk()).thenReturn(true); + + when(channel.retrieveMessageById(anyLong())).thenAnswer(inv -> { + long messageId = inv.getArgument(0); + String content = messages.get(messageId); + Message message = mockMessage( + content, Arrays.asList(embeds.getOrDefault(messageId, new MessageEmbed[0])), channel, messageId + ); + + return mockAction(message); + }); + when(channel.sendMessage(any(CharSequence.class))).thenAnswer(inv -> { + String content = inv.getArgument(0); + return mockReply(MessageCreateAction.class, mockMessage(content, channel)); + }); + when(channel.sendMessage(any(MessageCreateData.class))).thenAnswer(inv -> { + MessageCreateData data = inv.getArgument(0, MessageCreateData.class); + String content = data.getContent(); + List embeds = data.getEmbeds(); + + return mockReply(MessageCreateAction.class, mockMessage(content, embeds, channel)); + }); + when(channel.sendMessageFormat(anyString(), any(Object[].class))).thenAnswer(inv -> { + String content = String.format(inv.getArgument(0), (Object[]) inv.getRawArguments()[1]); + return mockReply(MessageCreateAction.class, mockMessage(content, channel)); + }); + when(channel.sendMessageEmbeds(any(), any(MessageEmbed[].class))).thenAnswer(inv -> { + MessageEmbed first = inv.getArgument(0); + + List embeds = new ArrayList<>(); + embeds.add(first); + if (inv.getArguments().length > 1) { + Object obj = inv.getArgument(1); + + if (obj instanceof MessageEmbed[] allEmbeds) + embeds.addAll(Arrays.asList(allEmbeds)); + else + embeds.add((MessageEmbed) obj); + } + + return mockReply(MessageCreateAction.class, mockMessage(null, embeds, channel)); + }); + + return channel; + } + + public static Message mockMessage(String content, MessageChannel channel) { + return mockMessage(content, channel, RANDOM.nextLong(0, Long.MAX_VALUE)); + } + + public static Message mockMessage(String content, MessageChannel channel, long id) { + Message message = JDAObjects.getMessage(content, channel); + messages.put(message.getIdLong(), content); + latestMessages.put(channel.getIdLong(), content); + latestEmbeds.remove(channel.getIdLong()); + + when(message.getContentRaw()).thenAnswer(inv -> messages.get(id)); + when(message.getJumpUrl()).thenReturn(""); + when(message.getIdLong()).thenReturn(id); + when(message.getId()).thenReturn(Long.toString(id)); + when(message.getGuild()).thenReturn(GUILD); + when(message.getMember()).thenReturn(SELF); + when(message.getChannel()).thenAnswer(inv -> mockUnion(channel)); + + when(message.getStartedThread()).thenReturn(null); + when(message.delete()).thenAnswer(inv -> { + messages.remove(id); + MockJDA.embeds.remove(id); + return mockAuditLog(); + }); + + when(message.editMessage(anyString())).thenAnswer(inv -> { + messages.put(id, inv.getArgument(0)); + return mockReply(MessageEditAction.class, message); + }); + when(message.editMessageEmbeds(any(MessageEmbed[].class))).thenAnswer(inv -> { + Object obj = inv.getArgument(0); + if (obj instanceof MessageEmbed[] allEmbeds) + embeds.put(id, allEmbeds); + else + embeds.put(id, new MessageEmbed[] { (MessageEmbed) obj }); + + return mockReply(MessageEditAction.class, message); + }); + when(message.editMessageEmbeds(anyCollection())).thenAnswer(inv -> { + Collection allEmbeds = inv.getArgument(0); + embeds.put(id, allEmbeds.toArray(new MessageEmbed[0])); + return mockReply(MessageEditAction.class, message); + }); + + when(message.createThreadChannel(anyString())).thenAnswer(inv -> mock(ThreadChannelAction.class)); + when(message.addReaction(any())).thenAnswer(inv -> mockAction(null)); + + return message; + } + + public static Message mockMessage(String content, List embeds, MessageChannel channel) { + return mockMessage(content, embeds, channel, RANDOM.nextLong(0, Long.MAX_VALUE)); + } + + public static Message mockMessage(String content, List embeds, MessageChannel channel, long id) { + Message message = mockMessage(content, channel, id); + MockJDA.embeds.put(message.getIdLong(), embeds.toArray(new MessageEmbed[0])); + latestEmbeds.put(channel.getIdLong(), embeds.toArray(new MessageEmbed[0])); + + when(message.getEmbeds()).thenAnswer(inv -> Arrays.asList(MockJDA.embeds.get(message.getIdLong()))); + + return message; + } + + private static Guild mockGuild() { + Guild guild = mock(Guild.class); + long serverId = CONFIG.getLong("server"); + + when(guild.getIdLong()).thenReturn(serverId); + when(guild.getJDA()).thenReturn(JDA); + when(guild.getTextChannels()).thenReturn(CHANNELS); + when(guild.getRoles()).thenReturn(ROLES); + when(guild.getSelfMember()).thenReturn(SELF); + + when(guild.addRoleToMember(any(UserSnowflake.class), any(Role.class))).thenAnswer(inv -> { + Member member = inv.getArgument(0); + Role role = inv.getArgument(1); + + member.getRoles().add(role); + return mockAuditLog(); + }); + when(guild.removeRoleFromMember(any(UserSnowflake.class), any(Role.class))).thenAnswer(inv -> { + Member member = inv.getArgument(0); + Role role = inv.getArgument(1); + + member.getRoles().remove(role); + return mockAuditLog(); + }); + when(guild.getRoleById(anyLong())).thenAnswer(inv -> { + long id = inv.getArgument(0); + return ROLES.stream().filter(role -> role.getIdLong() == id).findFirst().orElse(null); + }); + when(guild.getChannelById(any(), anyLong())).thenAnswer(inv -> { + long id = inv.getArgument(1); + return CHANNELS.stream().filter(channel -> channel.getIdLong() == id).findFirst().orElse(null); + }); + when(guild.getTextChannelById(anyLong())).thenAnswer(inv -> { + long id = inv.getArgument(0); + return CHANNELS.stream().filter(channel -> channel.getIdLong() == id).findFirst().orElse(null); + }); + when(guild.getTextChannelById(anyString())).thenAnswer(inv -> { + String id = inv.getArgument(0); + return CHANNELS.stream().filter(channel -> channel.getId().equals(id)).findFirst().orElse(null); + }); + when(guild.getMemberById(anyString())).thenAnswer(inv -> { + long id = Long.parseLong(inv.getArgument(0)); + return members.get(id); + }); + when(guild.getMemberById(anyLong())).thenAnswer(inv -> { + long id = inv.getArgument(0); + return members.get(id); + }); + + return guild; + } + + public static Member mockMember(String username) { + Member member = JDAObjects.getMember(username, "0000"); + + long id = RANDOM.nextLong(0, Long.MAX_VALUE); + members.put(id, member); + + when(member.getJDA()).thenReturn(JDA); + when(member.getGuild()).thenReturn(GUILD); + when(member.getId()).thenReturn(Long.toString(id)); + when(member.getIdLong()).thenReturn(id); + when(member.getAsMention()).thenReturn("<@" + id + ">"); + + List roles = new ArrayList<>(); + when(member.getRoles()).thenReturn(roles); + when(member.getUser().getEffectiveName()).thenReturn(username); + when(member.getUser().getIdLong()).thenReturn(id); + when(member.getUser().getAsMention()).thenReturn("<@" + id + ">"); + + return member; + } + + private static Role mockRole(String name, long id, int position) { + Role role = mock(Role.class); + + when(role.getJDA()).thenReturn(JDA); + when(role.getName()).thenReturn(name); + when(role.getIdLong()).thenReturn(id); + when(role.getGuild()).thenReturn(GUILD); + when(role.getColorRaw()).thenReturn(0); + when(role.getPosition()).thenReturn(position); + when(role.getPositionRaw()).thenReturn(0); + + return role; + } + + public static void assertEmbeds(long id, List expectedOutputs, boolean ignoreTimestamp) { + assertEmbeds(expectedOutputs, Arrays.asList(embeds.getOrDefault(id, new MessageEmbed[0])), ignoreTimestamp); + } + + + public static void assertEmbeds(List expectedOutputs, List embeds, boolean ignoreTimestamp) { + assertEquals(expectedOutputs.size(), embeds.size(), "Number of embeds"); + + int i = 0; + for (MessageEmbed embed : embeds) { + MessageEmbed expectedOutput = expectedOutputs.get(i); + assertEmbed(embed, expectedOutput, ignoreTimestamp); + i++; + } + } + + public static void assertEmbed(MessageEmbed embed, MessageEmbed expectedOutput, boolean ignoreTimestamp) { + Assertions.assertEquals(expectedOutput.getTitle(), embed.getTitle()); + Assertions.assertEquals(expectedOutput.getColor(), embed.getColor()); + Assertions.assertEquals(expectedOutput.getDescription(), embed.getDescription()); + Assertions.assertEquals(expectedOutput.getUrl(), embed.getUrl()); + if (expectedOutput.getAuthor() != null) + Assertions.assertEquals(expectedOutput.getAuthor().getName(), Objects.requireNonNull(embed.getAuthor()).getName()); + + if (expectedOutput.getFooter() != null) { + Assertions.assertEquals(expectedOutput.getFooter().getText(), Objects.requireNonNull(embed.getFooter()).getText()); + Assertions.assertEquals(expectedOutput.getFooter().getIconUrl(), Objects.requireNonNull(embed.getFooter()).getIconUrl()); + } + + if (expectedOutput.getImage() != null) + Assertions.assertEquals(expectedOutput.getImage().getUrl(), Objects.requireNonNull(embed.getImage()).getUrl()); + + if (expectedOutput.getThumbnail() != null) + Assertions.assertEquals(expectedOutput.getThumbnail().getUrl(), Objects.requireNonNull(embed.getThumbnail()).getUrl()); + + int i = 0; + for (MessageEmbed.Field field : embed.getFields()) { + try { + Assertions.assertEquals(expectedOutput.getFields().get(i).getName(), field.getName()); + Assertions.assertEquals(expectedOutput.getFields().get(i).getValue(), field.getValue()); + Assertions.assertEquals(expectedOutput.getFields().get(i).isInline(), field.isInline()); + } catch (IndexOutOfBoundsException e) { + Assertions.fail("Too many fields in embed: " + field.getName() + " - '" + field.getValue() + "'"); + } + i++; + } + + if (!ignoreTimestamp) + Assertions.assertEquals(expectedOutput.getTimestamp(), embed.getTimestamp()); + } + + public static long assertSlashCommandEvent(TestCommandListener listener, Map options, MessageEmbed... outputs) { + BotCommand command = listener.getCommand(); + SlashCommandEvent event = mockSlashCommandEvent(GENERAL, command, options); + return assertSlashCommandEvent(event, listener, outputs); + } + + public static long assertSlashCommandEvent(SlashCommandEvent event, TestCommandListener listener, MessageEmbed... outputs) { + listener.onEvent(event); + + if (outputs != null) + assertEmbeds(event.getIdLong(), Arrays.asList(outputs), true); + + return event.getIdLong(); + } + + public static SlashCommandEvent mockSlashCommandEvent(MessageChannel channel, BotCommand command, Map options) { + SlashCommandEvent event = mock(SlashCommandEvent.class); + long id = RANDOM.nextLong(0, Long.MAX_VALUE); + + when(event.getName()).thenAnswer(invocation -> command.getName()); + when(event.getSubcommandName()).thenAnswer(invocation -> command.getName()); + when(event.getSubcommandGroup()).thenAnswer(invocation -> command.getSubcommandGroup()); + when(event.getChannel()).thenAnswer(invocation -> channel); + when(event.getGuild()).thenAnswer(invocation -> GUILD); + when(event.getIdLong()).thenReturn(id); + + Member user = mockMember("User"); + GUILD.addRoleToMember(user, AUTHOR); + GUILD.addRoleToMember(user, REVIEWER); + GUILD.addRoleToMember(user, MAINTAINER); + GUILD.addRoleToMember(user, ADMINISTRATOR); + + when(event.getMember()).thenReturn(user); + when(event.getUser()).thenAnswer(inv -> user.getUser()); + when(event.getOptions()).thenAnswer(invocation -> options); + + when(event.getOption(anyString())).thenAnswer(invocation -> { + if (options == null) return null; + + OptionMapping mapping = mock(OptionMapping.class); + Object option = options.get(invocation.getArgument(0)); + + when(mapping.getAsAttachment()).thenReturn((Attachment) option); + when(mapping.getAsString()).thenReturn((String) option); + when(mapping.getAsBoolean()).thenReturn((Boolean) option); + when(mapping.getAsLong()).thenReturn((Long) option); + when(mapping.getAsInt()).thenReturn((Integer) option); + when(mapping.getAsDouble()).thenReturn((Double) option); + when(mapping.getAsMentionable()).thenReturn((IMentionable) option); + when(mapping.getAsRole()).thenReturn((Role) option); + when(mapping.getAsUser()).thenReturn((User) option); + when(mapping.getAsMember()).thenReturn((Member) option); + when(mapping.getAsChannel()).thenReturn((GuildChannelUnion) option); + + return mapping; + }); + when(event.getOption(anyString(), any(), any())).thenAnswer(inv -> { + if (options == null) return null; + + String key = inv.getArgument(0); + Object def = inv.getArgument(1); + + return options.getOrDefault(key, def); + }); + when(event.getOption(anyString(), isA(Object.class), any())).thenAnswer(inv -> { + if (options == null) return null; + + String key = inv.getArgument(0); + Object def = inv.getArgument(1); + + return options.getOrDefault(key, def); + }); + + mockReplyCallbacks(event, channel, id); + when(event.replyModal(any(Modal.class))).thenAnswer(inv -> { + CURRENT_MODAL = inv.getArgument(0); + return mockModalReply(user, channel); + }); + + return event; + } + + public static long assertButtonInteractionEvent(ListenerAdapter listener, Message message, Button button, MessageEmbed... outputs) { + ButtonInteractionEvent event = mockButtonInteractionEvent(message, button); + return assertButtonInteractionEvent(listener, event, outputs); + } + + public static long assertButtonInteractionEvent(ListenerAdapter listener, ButtonInteractionEvent event, MessageEmbed... outputs) { + listener.onButtonInteraction(event); + + if (outputs != null) + assertEmbeds(event.getIdLong(), Arrays.asList(outputs), true); + + return event.getIdLong(); + } + + public static ButtonInteractionEvent mockButtonInteractionEvent(Message message, Button button) { + ButtonInteractionEvent event = mock(ButtonInteractionEvent.class); + TextChannel channel = message.getChannel().asTextChannel(); + long id = RANDOM.nextLong(0, Long.MAX_VALUE); + + when(event.isFromGuild()).thenReturn(true); + when(event.getGuild()).thenReturn(GUILD); + when(event.getMessageChannel()).thenReturn(channel); + when(event.getButton()).thenReturn(button); + when(event.isFromGuild()).thenReturn(true); + + Member user = mockMember("User"); + GUILD.addRoleToMember(user, AUTHOR); + GUILD.addRoleToMember(user, REVIEWER); + GUILD.addRoleToMember(user, MAINTAINER); + GUILD.addRoleToMember(user, ADMINISTRATOR); + + when(event.getMember()).thenReturn(user); + when(event.getUser()).thenAnswer(inv -> user.getUser()); + when(event.getIdLong()).thenReturn(id); + + when(event.getMessage()).thenReturn(message); + when(event.getMessageId()).thenAnswer(inv -> message.getId()); + when(event.getMessageIdLong()).thenAnswer(inv -> message.getIdLong()); + + mockReplyCallbacks(event, channel, id); + when(event.replyModal(any(Modal.class))).thenAnswer(inv -> { + CURRENT_MODAL = inv.getArgument(0); + return mockModalReply(user, channel); + }); + + return event; + } + + public static long assertModalInteractionEvent(ListenerAdapter listener, Modal modal, MessageChannel channel, Map options, MessageEmbed... outputs) { + ModalInteractionEvent event = mockModalInteractionEvent(modal, channel, options); + return assertModalInteractionEvent(listener, event, outputs); + } + + public static long assertModalInteractionEvent(ListenerAdapter listener, ModalInteractionEvent event, MessageEmbed... outputs) { + listener.onModalInteraction(event); + + if (outputs != null) + assertEmbeds(event.getIdLong(), Arrays.asList(outputs), true); + + return event.getIdLong(); + } + + public static ModalInteractionEvent mockModalInteractionEvent(Modal modal, MessageChannel channel, Map options) { + ModalInteractionEvent event = mock(ModalInteractionEvent.class); + long id = RANDOM.nextLong(0, Long.MAX_VALUE); + + when(event.getJDA()).thenReturn(JDA); + when(event.isFromGuild()).thenReturn(true); + when(event.getGuild()).thenReturn(GUILD); + when(event.getModalId()).thenAnswer(inv -> modal.getId()); + when(event.getChannel()).thenAnswer(inv -> mockUnion(channel)); + when(event.getMessageChannel()).thenReturn(channel); + + when(event.getValue(anyString())).thenAnswer(inv -> { + String key = inv.getArgument(0); + if (!options.containsKey(key)) return null; + + ModalMapping mapping = mock(ModalMapping.class); + when(mapping.getId()).thenReturn(key); + when(mapping.getAsString()).thenReturn(options.get(key)); + + return mapping; + }); + + Member user = mockMember("User"); + GUILD.addRoleToMember(user, AUTHOR); + GUILD.addRoleToMember(user, REVIEWER); + GUILD.addRoleToMember(user, MAINTAINER); + GUILD.addRoleToMember(user, ADMINISTRATOR); + + when(event.getMember()).thenReturn(user); + when(event.getUser()).thenAnswer(inv -> user.getUser()); + when(event.getIdLong()).thenReturn(id); + + mockReplyCallbacks(event, channel, id); + return event; + } + + public static Modal mockModal(String id, String title) { + Modal modal = mock(Modal.class); + when(modal.getId()).thenReturn(id); + when(modal.getTitle()).thenReturn(title); + + return modal; + } + + private static void mockReplyCallbacks(IReplyCallback event, MessageChannel channel, long id) { + when(event.reply(anyString())).thenAnswer(invocation -> + mockReply(mockMessage(invocation.getArgument(0), channel, id))); + when(event.reply(any(MessageCreateData.class))).thenAnswer(invocation -> + mockReply(mockMessage(invocation.getArgument(0, MessageCreateData.class).getContent(), channel, id))); + when(event.replyEmbeds(anyList())).thenAnswer(invocation -> + mockReply(mockMessage(null, invocation.getArgument(0), channel, id))); + when(event.replyEmbeds(any(MessageEmbed.class), any(MessageEmbed[].class))).thenAnswer(invocation -> { + List embeds = invocation.getArguments().length == 1 ? new ArrayList<>() : Arrays.asList(invocation.getArgument(1)); + embeds.add(invocation.getArgument(0)); + return mockReply(mockMessage(null, embeds, channel, id)); + }); + + when(event.deferReply()).thenAnswer(inv -> mockReply(mockMessage(null, channel, id))); + when(event.deferReply(anyBoolean())).thenAnswer(inv -> mockReply(mockMessage(null, channel, id))); + } + + private static ReplyCallbackAction mockReply(Message message) { + ReplyCallbackAction action = mock(ReplyCallbackAction.class); + + messages.put(message.getIdLong(), message.getContentRaw()); + embeds.put(message.getIdLong(), message.getEmbeds().toArray(new MessageEmbed[0])); + + when(action.getEmbeds()).thenAnswer(inv -> embeds.get(message.getIdLong())); + + when(action.setEmbeds(anyCollection())).thenAnswer(inv -> { + Collection embed = inv.getArgument(0); + embeds.put(message.getIdLong(), embed.toArray(new MessageEmbed[0])); + return action; + }); + + when(action.addEmbeds(anyCollection())).thenAnswer(inv -> { + List total = new ArrayList<>(); + total.addAll(Arrays.asList(embeds.get(message.getIdLong()))); + total.addAll(inv.getArgument(0)); + embeds.put(message.getIdLong(), total.toArray(new MessageEmbed[0])); + return action; + }); + + doAnswer(inv -> { + Consumer hookConsumer = inv.getArgument(0); + hookConsumer.accept(mockInteractionHook(message.getMember(), message.getChannel(), InteractionType.COMMAND, message.getIdLong())); + return null; + }).when(action).queue(any()); + + when(action.setEphemeral(anyBoolean())).thenAnswer(invocation -> { + when(message.isEphemeral()).thenReturn(true); + return action; + }); + + return action; + } + + private static ModalCallbackAction mockModalReply(Member user, MessageChannel channel) { + ModalCallbackAction action = mock(ModalCallbackAction.class); + + doAnswer(inv -> { + Consumer hookConsumer = inv.getArgument(0); + hookConsumer.accept(mockInteractionHook(user, channel, InteractionType.MODAL_SUBMIT)); + return null; + }).when(action).queue(any()); + + return action; + } + + private static & RestAction> T mockWebhookReply(Class clazz, InteractionHook hook, Message message) { + T action = mock(clazz); + + doAnswer(inv -> { + Consumer hookConsumer = inv.getArgument(0); + hookConsumer.accept(hook); + return null; + }).when(action).queue(any()); + + when(action.getEmbeds()).thenAnswer(inv -> embeds.get(message.getIdLong())); + + when(action.setEmbeds(anyCollection())).thenAnswer(inv -> { + Collection embed = inv.getArgument(0); + embeds.put(message.getIdLong(), embed.toArray(new MessageEmbed[0])); + return action; + }); + + when(action.setEmbeds(any(MessageEmbed[].class))).thenAnswer(inv -> { + Object obj = inv.getArgument(0); + + if (obj instanceof MessageEmbed[] allEmbeds) + embeds.put(message.getIdLong(), allEmbeds); + else + embeds.put(message.getIdLong(), new MessageEmbed[] { (MessageEmbed) obj }); + return action; + }); + + return action; + } + + private static & RestAction> T mockReply(Class clazz, Message message) { + T action = mock(clazz); + + doAnswer(inv -> { + Consumer messageConsumer = inv.getArgument(0); + messageConsumer.accept(message); + return null; + }).when(action).queue(any()); + + doAnswer(inv -> { + Consumer messageConsumer = inv.getArgument(0); + Consumer errorConsumer = inv.getArgument(1); + + try { + messageConsumer.accept(message); + } catch (Exception e) { + errorConsumer.accept(e); + } + return null; + }).when(action).queue(any(), any()); + + when(action.getEmbeds()).thenAnswer(inv -> message.getEmbeds()); + when(action.setEmbeds(anyCollection())).thenAnswer(inv -> { + Collection embed = inv.getArgument(0); + embeds.put(message.getIdLong(), embed.toArray(new MessageEmbed[0])); + return action; + }); + + when(action.setActionRow(any(ItemComponent[].class))).thenAnswer(inv -> action); + when(action.setActionRow(anyCollection())).thenAnswer(inv -> action); + + if (action instanceof MessageEditAction edit) { + when(edit.setReplace(anyBoolean())).thenReturn(edit); + } + + return action; + } + + private static RestAction mockAction(T object) { + RestAction action = mock(RestAction.class); + when(action.complete()).thenReturn(object); + + doAnswer(inv -> { + Consumer consumer = inv.getArgument(0); + consumer.accept(object); + return null; + }).when(action).queue(any()); + + doAnswer(inv -> { + Consumer consumer = inv.getArgument(0); + Consumer error = inv.getArgument(1); + + try { + consumer.accept(object); + } catch (Exception e) { + error.accept(e); + } + return null; + }).when(action).queue(any(), any()); + + return action; + } + + private static AuditableRestAction mockAuditLog() { + AuditableRestAction action = mock(AuditableRestAction.class); + + doAnswer(inv -> { + Consumer consumer = inv.getArgument(0); + consumer.accept(null); + return null; + }).when(action).queue(any()); + + doAnswer(inv -> { + Consumer consumer = inv.getArgument(0); + Consumer error = inv.getArgument(1); + + try { + consumer.accept(null); + } catch (Exception e) { + error.accept(e); + } + return null; + }).when(action).queue(any(), any()); + + when(action.reason(any())).thenReturn(action); + + return action; + } + +} diff --git a/src/test/java/io/codemc/bot/commands/TestBotCommand.java b/src/test/java/io/codemc/bot/commands/TestBotCommand.java new file mode 100644 index 0000000..133ce1a --- /dev/null +++ b/src/test/java/io/codemc/bot/commands/TestBotCommand.java @@ -0,0 +1,65 @@ +package io.codemc.bot.commands; + +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import io.codemc.bot.MockCodeMCBot; +import io.codemc.bot.MockJDA; +import io.codemc.bot.utils.CommandUtil; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.interactions.InteractionHook; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TestBotCommand { + + private static MockBotCommand command; + + private static final class MockBotCommand extends BotCommand { + + MockBotCommand() { + super(MockCodeMCBot.INSTANCE); + } + + @Override + public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member) {} + + @Override + public void withModalReply(SlashCommandEvent event) {} + + } + + @BeforeAll + public static void init() { + command = new MockBotCommand(); + } + + @Test + @DisplayName("Test BotCommand Errors") + public void testErrors() { + TestCommandListener listener = new TestCommandListener(command); + + SlashCommandEvent e1 = MockJDA.mockSlashCommandEvent(MockJDA.GENERAL, command, Map.of()); + when(e1.getGuild()).thenReturn(null); + MockJDA.assertSlashCommandEvent(e1, listener, CommandUtil.embedError("Command can only be executed in a Server!")); + + SlashCommandEvent e2 = MockJDA.mockSlashCommandEvent(MockJDA.GENERAL, command, Map.of()); + when(e2.getGuild()).thenAnswer(inv -> { + Guild g = mock(Guild.class); + when(g.getIdLong()).thenReturn(123L); + return g; + }); + MockJDA.assertSlashCommandEvent(e2, listener, CommandUtil.embedError("Unable to find CodeMC Server!")); + + SlashCommandEvent e3 = MockJDA.mockSlashCommandEvent(MockJDA.GENERAL, command, Map.of()); + when(e3.getMember()).thenReturn(null); + + MockJDA.assertSlashCommandEvent(e3, listener, CommandUtil.embedError("Unable to retrieve Member from Event!")); + } + +} diff --git a/src/test/java/io/codemc/bot/commands/TestCmdApplication.java b/src/test/java/io/codemc/bot/commands/TestCmdApplication.java new file mode 100644 index 0000000..1b15bf3 --- /dev/null +++ b/src/test/java/io/codemc/bot/commands/TestCmdApplication.java @@ -0,0 +1,153 @@ +package io.codemc.bot.commands; + +import io.codemc.api.database.DatabaseAPI; +import io.codemc.api.jenkins.JenkinsAPI; +import io.codemc.api.nexus.NexusAPI; +import io.codemc.bot.MockCodeMCBot; +import io.codemc.bot.MockJDA; +import io.codemc.bot.commands.CmdApplication.Accept; +import io.codemc.bot.commands.CmdApplication.Deny; +import io.codemc.bot.utils.CommandUtil; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static io.codemc.bot.MockJDA.AUTHOR; +import static io.codemc.bot.MockJDA.REQUEST_CHANNEL; +import static org.junit.jupiter.api.Assertions.*; + +public class TestCmdApplication { + + private static CmdApplication command; + + @BeforeAll + public static void init() { + command = new CmdApplication(MockCodeMCBot.INSTANCE); + } + + @Test + @DisplayName("Test /application") + public void testApplication() { + assertEquals("application", command.getName()); + assertFalse(command.getHelp().isEmpty()); + assertEquals(0, command.getOptions().size()); + assertFalse(command.allowedRoles.isEmpty()); + assertTrue(command.getChildren().length > 0); + } + + @Test + @DisplayName("Test /application accept") + public void testAccept() { + Accept accept = (Accept) command.getChildren()[0]; + + assertEquals("accept", accept.getName()); + assertFalse(accept.getHelp().isEmpty()); + assertEquals(1, accept.getOptions().size()); + assertFalse(accept.allowedRoles.isEmpty()); + + TestCommandListener listener = new TestCommandListener(accept); + String username = "TestApplicationAccept"; + Member member = MockJDA.mockMember(username); + + MessageEmbed embed = CommandUtil.requestEmbed("[" + username + "](userLink)", "[Job](repoLink)", member.getAsMention(), "description", member.getId()); + Message message = MockJDA.mockMessage("", List.of(embed), REQUEST_CHANNEL); + + assertFalse(CommandUtil.hasRole(member, List.of(AUTHOR.getIdLong()))); + assertFalse(JenkinsAPI.existsUser(username)); + assertFalse(NexusAPI.exists(username)); + assertNull(DatabaseAPI.getUser(username)); + + long id = MockJDA.assertSlashCommandEvent(listener, Map.of("id", message.getId()), (MessageEmbed[]) null); + + String expected = String.format(""" + [5/5] Handling Join Request... + - [<:like:935126958193405962>] Message retrieved! + - [<:like:935126958193405962>] Message validated! + - Embed found! + - Found User ID `%s`. + - User and Repository Link found and validated! + - [<:like:935126958193405962>] `accepted-requests` channel found! + - [<:like:935126958193405962>] Join Request removed! + - Thread archived! + - Request Message deleted! + - [<:like:935126958193405962>] Gave User Role! + - Found Author Role! + - Applied Author Role to User! + + **Successfully accepted Join Request of user %s!** + """, member.getId(), member.getUser().getEffectiveName()); + + assertEquals(expected, MockJDA.getMessage(id)); + + assertTrue(CommandUtil.hasRole(member, List.of(AUTHOR.getIdLong()))); + assertTrue(JenkinsAPI.existsUser(username)); + assertTrue(NexusAPI.exists(username)); + assertNotNull(DatabaseAPI.getUser(username)); + assertEquals(member.getIdLong(), DatabaseAPI.getUser(username).getDiscord()); + + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedError("Message ID was not present!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("id", "abcd"), CommandUtil.embedError("Invalid message ID!")); + + assertTrue(JenkinsAPI.deleteUser(username)); + assertTrue(NexusAPI.deleteNexus(username)); + assertEquals(1, DatabaseAPI.removeUser(username)); + } + + @Test + @DisplayName("Test /application deny") + public void testDeny() { + Deny deny = (Deny) command.getChildren()[1]; + + assertEquals("deny", deny.getName()); + assertFalse(deny.getHelp().isEmpty()); + assertEquals(2, deny.getOptions().size()); + assertFalse(deny.allowedRoles.isEmpty()); + + TestCommandListener listener = new TestCommandListener(deny); + + String username = "TestApplicationDeny"; + Member member = MockJDA.mockMember(username); + + MessageEmbed embed = CommandUtil.requestEmbed("[" + username + "](userLink)", "[Job](repoLink)", member.getAsMention(), "description", member.getId()); + Message message = MockJDA.mockMessage("", List.of(embed), REQUEST_CHANNEL); + + assertFalse(CommandUtil.hasRole(member, List.of(AUTHOR.getIdLong()))); + assertFalse(JenkinsAPI.existsUser(username)); + assertFalse(NexusAPI.exists(username)); + assertNull(DatabaseAPI.getUser(username)); + + long id = MockJDA.assertSlashCommandEvent(listener, Map.of("id", message.getId(), "reason", "Denied"), (MessageEmbed[]) null); + + String expected = String.format( """ + [<:like:935126958193405962>] Handling of Join Request complete! + - [<:like:935126958193405962>] Message retrieved! + - [<:like:935126958193405962>] Message validated! + - Embed found! + - Found User ID `%s`. + - User and Repository Link found and validated! + - [<:like:935126958193405962>] `rejected-requests` channel found! + - [<:like:935126958193405962>] Join Request removed! + - Thread archived! + - Request Message deleted! + - [<:like:935126958193405962>] Finished rejecting join request of %s! + """, member.getId(), member.getUser().getEffectiveName()); + + assertEquals(expected, MockJDA.getMessage(id)); + + assertFalse(CommandUtil.hasRole(member, List.of(AUTHOR.getIdLong()))); + assertFalse(JenkinsAPI.existsUser(username)); + assertFalse(NexusAPI.exists(username)); + assertNull(DatabaseAPI.getUser(username)); + + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedError("Message ID was not present!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("id", "abcd"), CommandUtil.embedError("Invalid message ID!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("id", "0"), CommandUtil.embedError("Message ID or Reason were not present!")); + } + +} diff --git a/src/test/java/io/codemc/bot/commands/TestCmdCodeMC.java b/src/test/java/io/codemc/bot/commands/TestCmdCodeMC.java new file mode 100644 index 0000000..78a087b --- /dev/null +++ b/src/test/java/io/codemc/bot/commands/TestCmdCodeMC.java @@ -0,0 +1,432 @@ +package io.codemc.bot.commands; + +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import io.codemc.api.database.DatabaseAPI; +import io.codemc.api.jenkins.JenkinsAPI; +import io.codemc.api.nexus.NexusAPI; +import io.codemc.bot.MockCodeMCBot; +import io.codemc.bot.MockJDA; +import io.codemc.bot.commands.CmdCodeMC.*; +import io.codemc.bot.utils.CommandUtil; +import net.dv8tion.jda.api.entities.Member; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +public class TestCmdCodeMC { + + private static CmdCodeMC command; + + @BeforeAll + public static void init() { + command = new CmdCodeMC(MockCodeMCBot.INSTANCE); + + MockCodeMCBot.INSTANCE.create("CodeMC", "Bot"); + MockCodeMCBot.INSTANCE.create("CodeMC", "API"); + } + + @AfterAll + public static void cleanup() { + MockCodeMCBot.INSTANCE.delete("CodeMC"); + } + + @Test + @DisplayName("Test /codemc") + public void testCodemc() { + assertEquals("codemc", command.getName()); + assertFalse(command.getHelp().isEmpty()); + assertTrue(command.getChildren().length > 1); + } + + @Test + @DisplayName("Test /codemc jenkins") + public void testJenkins() { + Jenkins jenkins = (Jenkins) command.getChildren()[0]; + + assertEquals("jenkins", jenkins.getName()); + assertFalse(jenkins.getHelp().isEmpty()); + assertEquals(1, jenkins.getOptions().size()); + + TestCommandListener listener = new TestCommandListener(jenkins); + + MockJDA.assertSlashCommandEvent(listener, Map.of("job", "CodeMC/Bot"), jenkins.createInfoEmbed("CodeMC/Bot")); + MockJDA.assertSlashCommandEvent(listener, Map.of("job", "CodeMC/API"), jenkins.createInfoEmbed("CodeMC/API")); + + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedError("Invalid Jenkins Job provided!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("job", ""), CommandUtil.embedError("Invalid Jenkins Job provided!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("job", "InvalidJobName"), CommandUtil.embedError("Invalid Jenkins Job provided!")); + + MockJDA.assertSlashCommandEvent(listener, Map.of("job", "Inexistent/Inexistent"), CommandUtil.embedError("Failed to fetch Jenkins Job Info!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("job", "CodeMC/Inexistent"), CommandUtil.embedError("Failed to fetch Jenkins Job Info!")); + } + + @Test + @DisplayName("Test /codemc nexus") + public void testNexus() { + Nexus nexus = (Nexus) command.getChildren()[1]; + + assertEquals("nexus", nexus.getName()); + assertFalse(nexus.getHelp().isEmpty()); + assertEquals(1, nexus.getOptions().size()); + + TestCommandListener listener = new TestCommandListener(nexus); + + MockJDA.assertSlashCommandEvent(listener, Map.of("user", "CodeMC"), nexus.createInfoEmbed("CodeMC")); + + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedError("Invalid Username provided!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("user", ""), CommandUtil.embedError("Invalid Username provided!")); + + MockJDA.assertSlashCommandEvent(listener, Map.of("user", "Inexistent"), CommandUtil.embedError("Failed to fetch Nexus Repository Info!")); + } + + @Test + @DisplayName("Test /codemc remove") + public void testRemove() { + Remove remove = (Remove) command.getChildren()[2]; + + assertEquals("remove", remove.getName()); + assertFalse(remove.getHelp().isEmpty()); + assertEquals(1, remove.getOptions().size()); + assertFalse(remove.allowedRoles.isEmpty()); + + TestCommandListener listener = new TestCommandListener(remove); + + JenkinsAPI.createJenkinsUser("TestRemove", "1234"); + NexusAPI.createNexus("TestRemove", "1234"); + + assertTrue(JenkinsAPI.existsUser("TestRemove")); + assertTrue(NexusAPI.exists("TestRemove")); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestRemove"), CommandUtil.embedSuccess("Successfully removed TestRemove from the CodeMC Services!")); + + assertFalse(JenkinsAPI.existsUser("TestRemove")); + assertFalse(NexusAPI.exists("TestRemove")); + + MockCodeMCBot.INSTANCE.delete("TestRemove2"); + DatabaseAPI.removeUser("TestRemove2"); + Member m1 = MockJDA.mockMember("TestRemove2"); + MockJDA.GUILD.addRoleToMember(m1, MockJDA.AUTHOR); + + JenkinsAPI.createJenkinsUser("TestRemove2", "5678"); + NexusAPI.createNexus("TestRemove2", "5678"); + DatabaseAPI.addUser("TestRemove2", m1.getIdLong()); + + assertTrue(JenkinsAPI.existsUser("TestRemove2")); + assertTrue(NexusAPI.exists("TestRemove2")); + assertNotNull(DatabaseAPI.getUser("TestRemove2")); + assertEquals(m1.getIdLong(), DatabaseAPI.getUser("TestRemove2").getDiscord()); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestRemove2"), CommandUtil.embedSuccess("Revoked Author Status from TestRemove2!")); + + assertFalse(JenkinsAPI.existsUser("TestRemove2")); + assertFalse(NexusAPI.exists("TestRemove2")); + assertNull(DatabaseAPI.getUser("TestRemove2")); + + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedError("Invalid Jenkins User provided!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "Inexistent"), CommandUtil.embedError("The user does not have a Jenkins account!")); + + Member m2 = MockJDA.mockMember("TestRemove3"); + + JenkinsAPI.createJenkinsUser("TestRemove3", "1234"); + NexusAPI.createNexus("TestRemove3", "1234"); + DatabaseAPI.addUser("TestRemove3", m2.getIdLong()); + + assertTrue(JenkinsAPI.existsUser("TestRemove3")); + assertTrue(NexusAPI.exists("TestRemove3")); + assertNotNull(DatabaseAPI.getUser("TestRemove3")); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestRemove3"), CommandUtil.embedError("User was deleted, but is not an Author!")); + + assertFalse(JenkinsAPI.existsUser("TestRemove3")); + assertFalse(NexusAPI.exists("TestRemove3")); + assertNull(DatabaseAPI.getUser("TestRemove3")); + + JenkinsAPI.createJenkinsUser("TestRemove4", "1234"); + NexusAPI.createNexus("TestRemove4", "1234"); + DatabaseAPI.addUser("TestRemove4", -10L); + + assertTrue(JenkinsAPI.existsUser("TestRemove4")); + assertTrue(NexusAPI.exists("TestRemove4")); + assertNotNull(DatabaseAPI.getUser("TestRemove4")); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestRemove4"), CommandUtil.embedSuccess("Successfully removed TestRemove4 from the CodeMC Services!")); + + assertFalse(JenkinsAPI.existsUser("TestRemove4")); + assertFalse(NexusAPI.exists("TestRemove4")); + assertNull(DatabaseAPI.getUser("TestRemove4")); + } + + @Test + @DisplayName("Test /codemc validate") + public void testValidate() { + Validate validate = (Validate) command.getChildren()[3]; + + assertEquals("validate", validate.getName()); + assertFalse(validate.getHelp().isEmpty()); + assertEquals(1, validate.getOptions().size()); + assertFalse(validate.allowedRoles.isEmpty()); + + TestCommandListener listener = new TestCommandListener(validate); + + // Test One - Jenkins Only + + JenkinsAPI.createJenkinsUser("TestValidate_1", "1234"); + + assertTrue(JenkinsAPI.existsUser("TestValidate_1")); + assertFalse(NexusAPI.exists("TestValidate_1")); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestValidate_1"), CommandUtil.embedSuccess("Successfully validated 1 User(s)")); + + assertTrue(JenkinsAPI.existsUser("TestValidate_1")); + assertTrue(NexusAPI.exists("TestValidate_1")); + + MockCodeMCBot.INSTANCE.delete("TestValidate_1"); + + // Test One - Nexus Only + + NexusAPI.createNexus("TestValidate_2", "1234"); + + assertFalse(JenkinsAPI.existsUser("TestValidate_2")); + assertTrue(NexusAPI.exists("TestValidate_2")); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestValidate_2"), CommandUtil.embedSuccess("Successfully validated 1 User(s)")); + + assertTrue(JenkinsAPI.existsUser("TestValidate_2")); + assertTrue(NexusAPI.exists("TestValidate_2")); + + MockCodeMCBot.INSTANCE.delete("TestValidate_2"); + + // Test All - Jenkins Only + + JenkinsAPI.createJenkinsUser("TestValidate_30", "1234"); + JenkinsAPI.createJenkinsUser("TestValidate_31", "1234"); + JenkinsAPI.createJenkinsUser("TestValidate_32", "1234"); + + assertTrue(JenkinsAPI.existsUser("TestValidate_30")); + assertTrue(JenkinsAPI.existsUser("TestValidate_31")); + assertTrue(JenkinsAPI.existsUser("TestValidate_32")); + assertFalse(NexusAPI.exists("TestValidate_30")); + assertFalse(NexusAPI.exists("TestValidate_31")); + assertFalse(NexusAPI.exists("TestValidate_32")); + + int size1 = JenkinsAPI.getAllJenkinsUsers().size(); + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedSuccess("Successfully validated " + size1 + " User(s)")); + + assertTrue(JenkinsAPI.existsUser("TestValidate_30")); + assertTrue(JenkinsAPI.existsUser("TestValidate_31")); + assertTrue(JenkinsAPI.existsUser("TestValidate_32")); + assertTrue(NexusAPI.exists("TestValidate_30")); + assertTrue(NexusAPI.exists("TestValidate_31")); + assertTrue(NexusAPI.exists("TestValidate_32")); + + MockCodeMCBot.INSTANCE.delete("TestValidate_30"); + MockCodeMCBot.INSTANCE.delete("TestValidate_31"); + MockCodeMCBot.INSTANCE.delete("TestValidate_32"); + } + + @Test + @DisplayName("Test /codemc link") + public void testLink() { + Link link = (Link) command.getChildren()[4]; + + assertEquals("link", link.getName()); + assertFalse(link.getHelp().isEmpty()); + assertEquals(2, link.getOptions().size()); + assertFalse(link.allowedRoles.isEmpty()); + + TestCommandListener listener = new TestCommandListener(link); + + Member m1 = MockJDA.mockMember("TestLink"); + MockJDA.GUILD.addRoleToMember(m1, MockJDA.AUTHOR); + + MockCodeMCBot.INSTANCE.create("TestLink", "Job"); + + assertNull(DatabaseAPI.getUser("TestLink")); + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestLink", "discord", m1), CommandUtil.embedSuccess("Linked Discord User TestLink to Jenkins User TestLink!")); + assertNotNull(DatabaseAPI.getUser("TestLink")); + assertEquals(m1.getIdLong(), DatabaseAPI.getUser("TestLink").getDiscord()); + + Member m2 = MockJDA.mockMember("TestLink2"); + MockJDA.GUILD.addRoleToMember(m2, MockJDA.AUTHOR); + + MockCodeMCBot.INSTANCE.create("TestLink2", "Job"); + MockCodeMCBot.INSTANCE.create("TestLink3", "Job"); + + assertNull(DatabaseAPI.getUser("TestLink2")); + assertNull(DatabaseAPI.getUser("TestLink3")); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestLink2", "discord", m2), CommandUtil.embedSuccess("Linked Discord User TestLink2 to Jenkins User TestLink2!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestLink3", "discord", m2), CommandUtil.embedSuccess("Linked Discord User TestLink2 to Jenkins User TestLink3!")); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestLink", "discord", m1), CommandUtil.embedSuccess("Linked Discord User TestLink to Jenkins User TestLink!")); + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedError("Invalid Jenkins User provided!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "Inexistent", "discord", m1), CommandUtil.embedError("The user does not have a Jenkins account!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestLink"), CommandUtil.embedError("Invalid Discord User provided!")); + + MockJDA.GUILD.removeRoleFromMember(m1, MockJDA.AUTHOR); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestLink", "discord", m1), CommandUtil.embedError("The user is not an Author!")); + + MockCodeMCBot.INSTANCE.delete("TestLink"); + MockCodeMCBot.INSTANCE.delete("TestLink2"); + MockCodeMCBot.INSTANCE.delete("TestLink3"); + DatabaseAPI.removeUser("TestLink"); + DatabaseAPI.removeUser("TestLink2"); + DatabaseAPI.removeUser("TestLink3"); + } + + @Test + @DisplayName("Test /codemc unlink") + public void testUnlink() { + Unlink unlink = (Unlink) command.getChildren()[5]; + + assertEquals("unlink", unlink.getName()); + assertFalse(unlink.getHelp().isEmpty()); + assertEquals(2, unlink.getOptions().size()); + assertFalse(unlink.allowedRoles.isEmpty()); + + TestCommandListener listener = new TestCommandListener(unlink); + + Member m1 = MockJDA.mockMember("TestUnlink"); + MockJDA.GUILD.addRoleToMember(m1, MockJDA.AUTHOR); + + MockCodeMCBot.INSTANCE.create("TestUnlink", "Job"); + DatabaseAPI.addUser("TestUnlink", m1.getIdLong()); + + assertNotNull(DatabaseAPI.getUser("TestUnlink")); + MockJDA.assertSlashCommandEvent(listener, Map.of("discord", m1), CommandUtil.embedSuccess("Unlinked Discord User TestUnlink from their Jenkins/Nexus account!")); + assertNull(DatabaseAPI.getUser("TestUnlink")); + + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedError("Invalid Discord User provided!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("discord", m1), CommandUtil.embedError("The user is not linked to any Jenkins/Nexus account!")); + + MockCodeMCBot.INSTANCE.delete("TestUnlink"); + DatabaseAPI.removeUser("TestUnlink"); + + Member m2 = MockJDA.mockMember("TestUnlink2"); + MockJDA.GUILD.addRoleToMember(m2, MockJDA.AUTHOR); + + DatabaseAPI.removeUser("TestUnlink2"); + DatabaseAPI.removeUser("TestUnlink3"); + MockCodeMCBot.INSTANCE.create("TestUnlink2", "Job"); + MockCodeMCBot.INSTANCE.create("TestUnlink3", "Job"); + DatabaseAPI.addUser("TestUnlink2", m2.getIdLong()); + DatabaseAPI.addUser("TestUnlink3", m2.getIdLong()); + + assertNotNull(DatabaseAPI.getUser("TestUnlink2")); + assertNotNull(DatabaseAPI.getUser("TestUnlink3")); + MockJDA.assertSlashCommandEvent(listener, Map.of("discord", m2, "username", "TestUnlink3"),CommandUtil.embedSuccess("Unlinked Discord User TestUnlink2 from their Jenkins/Nexus account!")); + assertNotNull(DatabaseAPI.getUser("TestUnlink2")); + assertNull(DatabaseAPI.getUser("TestUnlink3")); + + MockCodeMCBot.INSTANCE.delete("TestUnlink2"); + MockCodeMCBot.INSTANCE.delete("TestUnlink3"); + DatabaseAPI.removeUser("TestUnlink2"); + DatabaseAPI.removeUser("TestUnlink3"); + } + + @Test + @DisplayName("Test /codemc change-password") + public void testChangePassword() { + ChangePassword changePassword = (ChangePassword) command.getChildren()[6]; + + assertEquals("change-password", changePassword.getName()); + assertFalse(changePassword.getHelp().isEmpty()); + assertEquals(1, changePassword.getOptions().size()); + + SlashCommandEvent event = MockJDA.mockSlashCommandEvent(MockJDA.REQUEST_CHANNEL, changePassword, Map.of()); + TestCommandListener listener = new TestCommandListener(changePassword); + + JenkinsAPI.createJenkinsUser("Bot", "1234"); + NexusAPI.createNexus("Bot", "1234"); + DatabaseAPI.addUser("Bot", event.getMember().getIdLong()); + + MockJDA.assertSlashCommandEvent(event, listener, CommandUtil.embedSuccess("Successfully changed your password!")); + + assertTrue(JenkinsAPI.deleteUser("Bot")); + assertTrue(NexusAPI.deleteNexus("Bot")); + + MockJDA.assertSlashCommandEvent(event, listener, CommandUtil.embedError("You do not have a Jenkins account!")); + + assertEquals(1, DatabaseAPI.removeUser("Bot")); + + MockJDA.assertSlashCommandEvent(event, listener, CommandUtil.embedError("You are not linked to any Jenkins/Nexus accounts!")); + + when(event.getMember().getRoles()).thenReturn(List.of()); + + MockJDA.assertSlashCommandEvent(event, listener, CommandUtil.embedError("Only Authors can regenerate their credentials.")); + } + + @Test + @DisplayName("Test /codemc createuser") + public void testCreateUser() { + CreateUser createUser = (CreateUser) command.getChildren()[7]; + + assertEquals("createuser", createUser.getName()); + assertFalse(createUser.getHelp().isEmpty()); + assertEquals(2, createUser.getOptions().size()); + assertFalse(createUser.allowedRoles.isEmpty()); + + TestCommandListener listener = new TestCommandListener(createUser); + + Member m1 = MockJDA.mockMember("TestCreateUser"); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestCreateUser", "discord", m1), CommandUtil.embedSuccess("Successfully created user TestCreateUser and linked it to " + m1.getUser().getEffectiveName() + "!")); + + Member m2 = MockJDA.mockMember("TestCreateUser2"); + MockCodeMCBot.INSTANCE.create("TestCreateUser2", "Job"); + DatabaseAPI.addUser("TestCreateUser2", m2.getIdLong()); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestCreateUser2", "discord", m2), CommandUtil.embedError("A user with that username already exists.")); + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedError("Invalid Username provided!")); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestCreateUser3"), CommandUtil.embedError("Invalid Discord User provided!")); + + MockCodeMCBot.INSTANCE.delete("TestCreateUser"); + MockCodeMCBot.INSTANCE.delete("TestCreateUser2"); + DatabaseAPI.removeUser("TestCreateUser"); + DatabaseAPI.removeUser("TestCreateUser2"); + } + + @Test + @DisplayName("Test /codemc deluser") + public void testDelUser() { + DeleteUser delUser = (DeleteUser) command.getChildren()[8]; + + assertEquals("deluser", delUser.getName()); + assertFalse(delUser.getHelp().isEmpty()); + assertEquals(1, delUser.getOptions().size()); + assertFalse(delUser.allowedRoles.isEmpty()); + + TestCommandListener listener = new TestCommandListener(delUser); + + Member m1 = MockJDA.mockMember("TestDelUser"); + + MockCodeMCBot.INSTANCE.create("TestDelUser", "Job"); + DatabaseAPI.addUser("TestDelUser", m1.getIdLong()); + + assertTrue(JenkinsAPI.existsUser("TestDelUser")); + assertTrue(NexusAPI.exists("TestDelUser")); + assertNotNull(DatabaseAPI.getUser("TestDelUser")); + assertEquals(m1.getIdLong(), DatabaseAPI.getUser("TestDelUser").getDiscord()); + + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "TestDelUser"), CommandUtil.embedSuccess("Successfully deleted user TestDelUser!")); + + assertFalse(JenkinsAPI.existsUser("TestDelUser")); + assertFalse(NexusAPI.exists("TestDelUser")); + assertNull(DatabaseAPI.getUser("TestDelUser")); + + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedError("Invalid Username provided!")); + MockJDA.assertSlashCommandEvent(listener, Map.of("username", "Inexistent"), CommandUtil.embedError("The user does not exist!")); + + DatabaseAPI.removeUser("TestDelUser"); + } + +} diff --git a/src/test/java/io/codemc/bot/commands/TestCmdDisable.java b/src/test/java/io/codemc/bot/commands/TestCmdDisable.java new file mode 100644 index 0000000..7c02c3d --- /dev/null +++ b/src/test/java/io/codemc/bot/commands/TestCmdDisable.java @@ -0,0 +1,25 @@ +package io.codemc.bot.commands; + +import io.codemc.bot.MockCodeMCBot; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class TestCmdDisable { + + @Test + @DisplayName("Test /disable") + public void testDisable() { + CmdDisable command = new CmdDisable(MockCodeMCBot.INSTANCE); + + assertEquals("disable", command.getName()); + assertFalse(command.getHelp().isEmpty()); + assertEquals(0, command.getOptions().size()); + assertFalse(command.allowedRoles.isEmpty()); + + // Command calls System.exit - cannot be tested + } + +} diff --git a/src/test/java/io/codemc/bot/commands/TestCmdMsg.java b/src/test/java/io/codemc/bot/commands/TestCmdMsg.java new file mode 100644 index 0000000..ee3a4ef --- /dev/null +++ b/src/test/java/io/codemc/bot/commands/TestCmdMsg.java @@ -0,0 +1,81 @@ +package io.codemc.bot.commands; + +import io.codemc.bot.MockCodeMCBot; +import io.codemc.bot.MockJDA; +import io.codemc.bot.commands.CmdMsg.Edit; +import io.codemc.bot.commands.CmdMsg.Post; +import io.codemc.bot.utils.CommandUtil; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static io.codemc.bot.MockJDA.GENERAL; +import static org.junit.jupiter.api.Assertions.*; + +public class TestCmdMsg { + + private static CmdMsg command; + + @BeforeAll + public static void setup() { + command = new CmdMsg(MockCodeMCBot.INSTANCE); + } + + @Test + @DisplayName("Test /msg") + public void testMsg() { + assertEquals("msg", command.getName()); + assertFalse(command.getHelp().isEmpty()); + assertTrue(command.getChildren().length > 1); + } + + @Test + @DisplayName("Test /msg post") + public void testPost() { + Post post = new Post(MockCodeMCBot.INSTANCE); + + assertEquals("send", post.getName()); + assertFalse(post.getHelp().isEmpty()); + assertFalse(post.allowedRoles.isEmpty()); + assertTrue(post.hasModalReply); + assertEquals(2, post.getOptions().size()); + + TestCommandListener listener = new TestCommandListener(post); + + MockJDA.assertSlashCommandEvent(listener, Map.of("channel", GENERAL, "embed", true), (MessageEmbed[]) null); + MockJDA.assertSlashCommandEvent(listener, Map.of("channel", GENERAL, "embed", false), (MessageEmbed[]) null); + + assertNotNull(MockJDA.CURRENT_MODAL); + + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedError("Received invalid Channel input.")); + } + + @Test + @DisplayName("Test /msg edit") + public void testEdit() { + Edit edit = new Edit(MockCodeMCBot.INSTANCE); + + assertEquals("edit", edit.getName()); + assertFalse(edit.getHelp().isEmpty()); + assertFalse(edit.allowedRoles.isEmpty()); + assertTrue(edit.hasModalReply); + assertEquals(3, edit.getOptions().size()); + + TestCommandListener listener = new TestCommandListener(edit); + Message message = MockJDA.mockMessage("", GENERAL); + + MockJDA.assertSlashCommandEvent(listener, Map.of("channel", GENERAL, "id", message.getIdLong(), "embed", true), (MessageEmbed[]) null); + MockJDA.assertSlashCommandEvent(listener, Map.of("channel", GENERAL, "id", message.getIdLong(), "embed", false), (MessageEmbed[]) null); + + assertNotNull(MockJDA.CURRENT_MODAL); + + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedError("Received invalid Channel or Message ID.")); + MockJDA.assertSlashCommandEvent(listener, Map.of("channel", GENERAL), CommandUtil.embedError("Received invalid Channel or Message ID.")); + MockJDA.assertSlashCommandEvent(listener, Map.of("id", message.getIdLong()), CommandUtil.embedError("Received invalid Channel or Message ID.")); + } + +} diff --git a/src/test/java/io/codemc/bot/commands/TestCmdReload.java b/src/test/java/io/codemc/bot/commands/TestCmdReload.java new file mode 100644 index 0000000..211b6a4 --- /dev/null +++ b/src/test/java/io/codemc/bot/commands/TestCmdReload.java @@ -0,0 +1,33 @@ +package io.codemc.bot.commands; + +import io.codemc.bot.MockCodeMCBot; +import io.codemc.bot.MockJDA; +import io.codemc.bot.utils.CommandUtil; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class TestCmdReload { + + @Test + @DisplayName("Test /reload") + public void testReload() { + CmdReload command = new CmdReload(MockCodeMCBot.INSTANCE); + + assertEquals("reload", command.getName()); + assertFalse(command.getHelp().isEmpty()); + assertEquals(0, command.getOptions().size()); + assertFalse(command.allowedRoles.isEmpty()); + + TestCommandListener listener = new TestCommandListener(command); + + MockJDA.assertSlashCommandEvent(listener, Map.of(), CommandUtil.embedSuccess("Reload success!")); + + MockCodeMCBot.INSTANCE.setTestConfig(); + } + +} \ No newline at end of file diff --git a/src/test/java/io/codemc/bot/commands/TestCmdSubmit.java b/src/test/java/io/codemc/bot/commands/TestCmdSubmit.java new file mode 100644 index 0000000..1a290b5 --- /dev/null +++ b/src/test/java/io/codemc/bot/commands/TestCmdSubmit.java @@ -0,0 +1,35 @@ +package io.codemc.bot.commands; + +import io.codemc.bot.MockCodeMCBot; +import io.codemc.bot.MockJDA; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestCmdSubmit { + + private static CmdSubmit command; + + @BeforeAll + public static void init() { + command = new CmdSubmit(MockCodeMCBot.INSTANCE); + } + + @Test + @DisplayName("Test /submit") + public void testSubmit() { + assertEquals("submit", command.getName()); + assertEquals(0, command.getOptions().size()); + assertTrue(command.hasModalReply); + + TestCommandListener listener = new TestCommandListener(command); + + MockJDA.assertSlashCommandEvent(listener, null); + assertNotNull(MockJDA.CURRENT_MODAL); + assertEquals("submit", MockJDA.CURRENT_MODAL.getId()); + assertEquals(4, MockJDA.CURRENT_MODAL.getComponents().size()); + } + +} diff --git a/src/test/java/io/codemc/bot/commands/TestCommandListener.java b/src/test/java/io/codemc/bot/commands/TestCommandListener.java new file mode 100644 index 0000000..191107b --- /dev/null +++ b/src/test/java/io/codemc/bot/commands/TestCommandListener.java @@ -0,0 +1,26 @@ +package io.codemc.bot.commands; + +import com.jagrosh.jdautilities.command.SlashCommand; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.hooks.EventListener; + +public class TestCommandListener implements EventListener { + + private final BotCommand command; + + public TestCommandListener(SlashCommand command) { + this.command = (BotCommand) command; + } + + public BotCommand getCommand() { + return command; + } + + @Override + public void onEvent(GenericEvent event) { + if (event instanceof SlashCommandEvent commandEvent) + command.execute(commandEvent); + } + +} \ No newline at end of file diff --git a/src/test/java/io/codemc/bot/config/TestConfigHandler.java b/src/test/java/io/codemc/bot/config/TestConfigHandler.java new file mode 100644 index 0000000..e71775c --- /dev/null +++ b/src/test/java/io/codemc/bot/config/TestConfigHandler.java @@ -0,0 +1,67 @@ +package io.codemc.bot.config; + +import io.codemc.bot.MockCodeMCBot; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class TestConfigHandler { + + private final ConfigHandler handler = MockCodeMCBot.INSTANCE.getConfigHandler(); + + @Test + @DisplayName("Test ConfigHandler#getString") + public void testGetString() { + assertEquals("TOKEN", handler.getString("bot_token")); + + assertEquals("admin", handler.getString("jenkins", "username")); + assertEquals("http://localhost:8080", handler.getString("jenkins", "url")); + + assertEquals("test", handler.getString("database", "database")); + assertEquals("localhost", handler.getString("database", "host")); + assertEquals("admin", handler.getString("database", "username")); + + assertNotEquals("unset", handler.getString("nexus", "password")); + } + + @Test + @DisplayName("Test ConfigHandler#getLong") + public void testGetLong() { + assertEquals(405915656039694336L, handler.getLong("server")); + assertEquals(405918641859723294L, handler.getLong("author_role")); + + assertEquals(204232208049766400L, handler.getLong("users", "owner")); + assertEquals(1233971297185431582L, handler.getLong("channels", "request_access")); + assertEquals(784119059138478080L, handler.getLong("channels", "accepted_requests")); + assertEquals(800423355551449098L, handler.getLong("channels", "rejected_requests")); + } + + @Test + @DisplayName("Test ConfigHandler#getStringList") + public void testGetStringList() { + assertEquals(4, handler.getStringList("messages", "accepted").size()); + assertEquals("Your request has been **accepted**!", handler.getStringList("messages", "accepted").get(0)); + + assertEquals(4, handler.getStringList("messages", "denied").size()); + assertEquals("Your request has been **rejected**!", handler.getStringList("messages", "denied").get(0)); + } + + @Test + @DisplayName("Test ConfigHandler#getLongList") + public void testGetLongList() { + assertEquals(3, handler.getLongList("allowed_roles", "applications", "accept").size()); + assertEquals(405917902865170453L, handler.getLongList("allowed_roles", "applications", "accept").get(0)); + + assertEquals(3, handler.getLongList("allowed_roles", "applications", "deny").size()); + assertEquals(405917902865170453L, handler.getLongList("allowed_roles", "applications", "deny").get(0)); + } + + @Test + @DisplayName("Test ConfigHandler#getInt") + public void testGetInt() { + assertEquals(3306, handler.getInt("database", "port")); + } + +} diff --git a/src/test/java/io/codemc/bot/listeners/TestButtonListener.java b/src/test/java/io/codemc/bot/listeners/TestButtonListener.java new file mode 100644 index 0000000..a0d30c5 --- /dev/null +++ b/src/test/java/io/codemc/bot/listeners/TestButtonListener.java @@ -0,0 +1,131 @@ +package io.codemc.bot.listeners; + +import io.codemc.api.database.DatabaseAPI; +import io.codemc.api.jenkins.JenkinsAPI; +import io.codemc.api.nexus.NexusAPI; +import io.codemc.bot.MockCodeMCBot; +import io.codemc.bot.MockJDA; +import io.codemc.bot.utils.CommandUtil; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static io.codemc.bot.MockJDA.AUTHOR; +import static io.codemc.bot.MockJDA.REQUEST_CHANNEL; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TestButtonListener { + + private static ButtonListener listener; + + @BeforeAll + public static void init() { + listener = new ButtonListener(MockCodeMCBot.INSTANCE); + } + + @Test + @DisplayName("Test Application Accept") + public void testApplicationAccept() { + String username = "TestButtonListenerAccept"; + Member member = MockJDA.mockMember(username); + + MessageEmbed embed = CommandUtil.requestEmbed("[" + username + "](userLink)", "[Job](repoLink)", member.getAsMention(), "description", member.getId()); + Message message = MockJDA.mockMessage("", List.of(embed), REQUEST_CHANNEL); + + JenkinsAPI.deleteUser(username); + NexusAPI.deleteNexus(username); + DatabaseAPI.removeUser(username); + + assertFalse(CommandUtil.hasRole(member, List.of(AUTHOR.getIdLong()))); + assertFalse(JenkinsAPI.existsUser(username)); + assertFalse(NexusAPI.exists(username)); + assertNull(DatabaseAPI.getUser(username)); + + MockJDA.assertButtonInteractionEvent(listener, message, Button.success("application:accept:" + username + ":Job", "Accept"), (MessageEmbed[]) null); + + assertTrue(CommandUtil.hasRole(member, List.of(AUTHOR.getIdLong()))); + assertTrue(JenkinsAPI.existsUser(username)); + assertTrue(NexusAPI.exists(username)); + assertNotNull(DatabaseAPI.getUser(username)); + assertEquals(member.getIdLong(), DatabaseAPI.getUser(username).getDiscord()); + + assertTrue(JenkinsAPI.deleteUser(username)); + assertTrue(NexusAPI.deleteNexus(username)); + assertEquals(1, DatabaseAPI.removeUser(username)); + } + + @Test + @DisplayName("Test Application Deny") + public void testApplicationDeny() { + String username = "TestButtonListenerDeny"; + Member member = MockJDA.mockMember(username); + + MessageEmbed embed = CommandUtil.requestEmbed("[" + username + "](userLink)", "[Job](repoLink)", member.getAsMention(), "description", member.getId()); + Message message = MockJDA.mockMessage("", List.of(embed), REQUEST_CHANNEL); + + JenkinsAPI.deleteUser(username); + NexusAPI.deleteNexus(username); + DatabaseAPI.removeUser(username); + + assertFalse(CommandUtil.hasRole(member, List.of(AUTHOR.getIdLong()))); + assertFalse(JenkinsAPI.existsUser(username)); + assertFalse(NexusAPI.exists(username)); + + MockJDA.assertButtonInteractionEvent(listener, message, Button.danger("application:deny:" + username + ":Job", "Deny"), (MessageEmbed[]) null); + + assertFalse(CommandUtil.hasRole(member, List.of(AUTHOR.getIdLong()))); + assertFalse(JenkinsAPI.existsUser(username)); + assertFalse(NexusAPI.exists(username)); + } + + @Test + @DisplayName("Test ButtonListener Errors") + public void testButtonListenerErrors() { + Message m1 = MockJDA.mockMessage(null, REQUEST_CHANNEL); + ButtonInteractionEvent e1 = MockJDA.mockButtonInteractionEvent(m1, Button.primary("null", "null")); + when(e1.getGuild()).thenReturn(null); + MockJDA.assertButtonInteractionEvent(listener, e1, CommandUtil.embedError("Buttons only work on the CodeMC Server!")); + + Message m2 = MockJDA.mockMessage(null, REQUEST_CHANNEL); + Button b2 = mock(Button.class); + when(b2.getId()).thenReturn(null); + ButtonInteractionEvent e2 = MockJDA.mockButtonInteractionEvent(m2, b2); + MockJDA.assertButtonInteractionEvent(listener, e2, CommandUtil.embedError("Received Button Interaction with no ID!")); + + Message m3 = MockJDA.mockMessage(null, REQUEST_CHANNEL); + ButtonInteractionEvent e3 = MockJDA.mockButtonInteractionEvent(m3, Button.primary("null", "null")); + when(e3.getMember()).thenReturn(null); + MockJDA.assertButtonInteractionEvent(listener, e3, CommandUtil.embedError("Cannot get Member from Server!")); + + Message m4 = MockJDA.mockMessage(null, REQUEST_CHANNEL); + ButtonInteractionEvent e4 = MockJDA.mockButtonInteractionEvent(m4, Button.primary("null", "null")); + MockJDA.assertButtonInteractionEvent(listener, e4, CommandUtil.embedError("Received non-application button event!")); + + Message m5 = MockJDA.mockMessage(null, REQUEST_CHANNEL); + ButtonInteractionEvent e5 = MockJDA.mockButtonInteractionEvent(m5, Button.primary("application:null:null:null", "null")); + MockJDA.assertButtonInteractionEvent(listener, e5, CommandUtil.embedError("Received unknown Button Application type.", "Expected `accept` or `deny` but got `null`.")); + } + + @Test + @DisplayName("Test ButtonListener#lacksRole") + public void testLacksRole() { + assertTrue(listener.lacksRole(List.of(), List.of())); + + assertTrue(listener.lacksRole(List.of(1L, 2L, 3L), List.of(4L))); + assertTrue(listener.lacksRole(List.of(1L, 2L, 3L), List.of(4L, 5L))); + assertTrue(listener.lacksRole(List.of(2L, 3L, 4L), List.of(5L, 6L, 7L))); + + assertFalse(listener.lacksRole(List.of(1L, 2L, 3L), List.of(1L))); + assertFalse(listener.lacksRole(List.of(1L, 2L, 3L), List.of(1L, 2L))); + } + +} diff --git a/src/test/java/io/codemc/bot/listeners/TestModalListener.java b/src/test/java/io/codemc/bot/listeners/TestModalListener.java new file mode 100644 index 0000000..ac133f7 --- /dev/null +++ b/src/test/java/io/codemc/bot/listeners/TestModalListener.java @@ -0,0 +1,172 @@ +package io.codemc.bot.listeners; + +import io.codemc.api.jenkins.JenkinsAPI; +import io.codemc.bot.MockCodeMCBot; +import io.codemc.bot.MockJDA; +import io.codemc.bot.utils.CommandUtil; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.interactions.modals.Modal; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static io.codemc.bot.MockJDA.GENERAL; +import static io.codemc.bot.MockJDA.REQUEST_CHANNEL; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +public class TestModalListener { + + private static ModalListener listener; + + @BeforeAll + public static void init() { + listener = new ModalListener(MockCodeMCBot.INSTANCE); + } + + + @Test + @DisplayName("Test Submit") + public void testSubmit() { + String username = "TestModalListenerSubmit"; + Modal modal = MockJDA.mockModal("submit", "Submit"); + Map options = Map.of( + "user", username, + "repo", "Job", + "description", "description", + "repoLink", "repoLink" + ); + + MockJDA.assertModalInteractionEvent(listener, modal, GENERAL, options, CommandUtil.embedSuccess("[Request sent!]()")); + MockJDA.assertModalInteractionEvent(listener, modal, GENERAL, Map.of("user", username, "repo", "Job", "description", "description"), CommandUtil.embedSuccess("[Request sent!]()")); + + MockJDA.assertModalInteractionEvent(listener, modal, GENERAL, Map.of(), CommandUtil.embedError("The provided user was invalid.")); + + JenkinsAPI.createJenkinsUser(username, "1234"); + MockJDA.assertModalInteractionEvent(listener, modal, GENERAL, options, CommandUtil.embedError("A Jenkins User named '" + username + "' already exists!")); + JenkinsAPI.deleteUser(username); + + MockJDA.assertModalInteractionEvent( + listener, modal, GENERAL, Map.of("user", username), + CommandUtil.embedError("The option User, Repo and/or Description was not set properly!") + ); + MockJDA.assertModalInteractionEvent( + listener, modal, GENERAL, Map.of("user", username, "description", "Description"), + CommandUtil.embedError("The option User, Repo and/or Description was not set properly!") + ); + MockJDA.assertModalInteractionEvent( + listener, modal, GENERAL, Map.of("user", username, "repo", "Job"), + CommandUtil.embedError("The option User, Repo and/or Description was not set properly!") + ); + MockJDA.assertModalInteractionEvent( + listener, modal, GENERAL, Map.of("user", username, "repo", "", "description", "Description"), + CommandUtil.embedError("The option User, Repo and/or Description was not set properly!") + ); + MockJDA.assertModalInteractionEvent( + listener, modal, GENERAL, Map.of("user", username, "repo", "Job", "description", ""), + CommandUtil.embedError("The option User, Repo and/or Description was not set properly!") + ); + } + + @Test + @DisplayName("Test Deny Application") + public void testDenyApplication() { + String username = "TestModalApplicationDeny"; + Member member = MockJDA.mockMember(username); + + MessageEmbed embed = CommandUtil.requestEmbed("[" + username + "](userLink)", "[Job](repoLink)", member.getAsMention(), "description", member.getId()); + + Message message = MockJDA.mockMessage("", List.of(embed), REQUEST_CHANNEL); + Message message2 = MockJDA.mockMessage("", List.of(embed), REQUEST_CHANNEL); + + Modal m1 = MockJDA.mockModal("deny_application:" + message.getId(), "Deny Application"); + Modal m2 = MockJDA.mockModal("deny_application:" + message2.getId(), "Deny Application"); + + long id1 = MockJDA.assertModalInteractionEvent(listener, m1, REQUEST_CHANNEL, Map.of(), (MessageEmbed[]) null); + long id2 = MockJDA.assertModalInteractionEvent(listener, m2, REQUEST_CHANNEL, Map.of("reason", "reason"), (MessageEmbed[]) null); + + String expected = String.format(""" + [<:like:935126958193405962>] Handling of Join Request complete! + - [<:like:935126958193405962>] Message retrieved! + - [<:like:935126958193405962>] Message validated! + - Embed found! + - Found User ID `%s`. + - User and Repository Link found and validated! + - [<:like:935126958193405962>] `rejected-requests` channel found! + - [<:like:935126958193405962>] Join Request removed! + - Thread archived! + - Request Message deleted! + - [<:like:935126958193405962>] Finished rejecting join request of %s! + """, member.getId(), member.getUser().getEffectiveName()); + + assertEquals(expected, MockJDA.getMessage(id1)); + assertEquals(expected, MockJDA.getMessage(id2)); + + Modal m3 = MockJDA.mockModal("deny_application", "Deny Application"); + MockJDA.assertModalInteractionEvent(listener, m3, REQUEST_CHANNEL, Map.of(), CommandUtil.embedError("Received invalid Deny Application modal!")); + + Modal m4 = MockJDA.mockModal("deny_application:abcd", "Deny Application"); + MockJDA.assertModalInteractionEvent(listener, m4, REQUEST_CHANNEL, Map.of(), CommandUtil.embedError("Received invalid message ID: abcd")); + } + + @Test + @DisplayName("Test Message") + public void testMessage() { + String channelId = REQUEST_CHANNEL.getId(); + + // Test Post + Modal m1 = MockJDA.mockModal("message:post:" + channelId + ":true", "Message"); + MockJDA.assertModalInteractionEvent(listener, m1, GENERAL, Map.of("message", "test"), CommandUtil.embedSuccess("[Message sent!]()")); + + Modal m2 = MockJDA.mockModal("message:post:" + channelId + ":false", "Message"); + MockJDA.assertModalInteractionEvent(listener, m2, GENERAL, Map.of("message", "test"), CommandUtil.embedSuccess("[Message sent!]()")); + + // Test Edit + Message msg1 = MockJDA.mockMessage("Message", GENERAL); + Modal m3 = MockJDA.mockModal("message:edit:" + channelId + ":true:" + msg1.getId(), "Message"); + MockJDA.assertModalInteractionEvent(listener, m3, GENERAL, Map.of("message", "test"), CommandUtil.embedSuccess("[Message edited!]()")); + + Message msg2 = MockJDA.mockMessage("Message", GENERAL); + Modal m4 = MockJDA.mockModal("message:edit:" + channelId + ":false:" + msg2.getId(), "Message"); + MockJDA.assertModalInteractionEvent(listener, m4, GENERAL, Map.of("message", "test"), CommandUtil.embedSuccess("[Message edited!]()")); + + // Test Errors + Modal m5 = MockJDA.mockModal("message", "Message"); + MockJDA.assertModalInteractionEvent(listener, m5, GENERAL, Map.of(), CommandUtil.embedError(("Invalid Modal data. Expected `4+` arguments but received `1`!"))); + + Modal m6 = MockJDA.mockModal("message:post:abcd:true", "Message"); + MockJDA.assertModalInteractionEvent(listener, m6, GENERAL, Map.of(), CommandUtil.embedError("Received invalid Text Channel.")); + + Modal m7 = MockJDA.mockModal("message:post:" + channelId + ":true", "Message"); + MockJDA.assertModalInteractionEvent(listener, m7, GENERAL, Map.of(), CommandUtil.embedError("Received invalid Message to sent/edit.")); + + Modal m8 = MockJDA.mockModal("message:edit:" + channelId + ":true", "Message"); + MockJDA.assertModalInteractionEvent(listener, m8, GENERAL, Map.of("message", "test"), CommandUtil.embedError("Received invalid Modal data. Expected `>4` but got `=4`")); + + Modal m9 = MockJDA.mockModal("message:edit:" + channelId + ":true:abcd", "Message"); + MockJDA.assertModalInteractionEvent(listener, m9, GENERAL, Map.of("message", "test"), CommandUtil.embedError("Received invalid message ID `abcd`.")); + + Modal m10 = MockJDA.mockModal("message:unknown:" + channelId + ":true", "Message"); + MockJDA.assertModalInteractionEvent(listener, m10, GENERAL, Map.of("message", "test"), CommandUtil.embedError("Received Unknown Message type: `unknown`.")); + } + + @Test + @DisplayName("Test ModalListener Errors") + public void testModalListenerErrors() { + Modal modal = MockJDA.mockModal("null", "null"); + + ModalInteractionEvent e1 = MockJDA.mockModalInteractionEvent(modal, REQUEST_CHANNEL, Map.of()); + when(e1.getGuild()).thenReturn(null); + MockJDA.assertModalInteractionEvent(listener, e1, CommandUtil.embedError("Unable to retrieve CodeMC Server!")); + + ModalInteractionEvent e2 = MockJDA.mockModalInteractionEvent(modal, REQUEST_CHANNEL, Map.of()); + MockJDA.assertModalInteractionEvent(listener, e2, CommandUtil.embedError("Received Modal with unknown ID `null`.")); + } + +} diff --git a/src/test/java/io/codemc/bot/utils/TestAPIUtil.java b/src/test/java/io/codemc/bot/utils/TestAPIUtil.java new file mode 100644 index 0000000..ae1a0dd --- /dev/null +++ b/src/test/java/io/codemc/bot/utils/TestAPIUtil.java @@ -0,0 +1,173 @@ +package io.codemc.bot.utils; + +import io.codemc.api.jenkins.JenkinsAPI; +import io.codemc.api.nexus.NexusAPI; +import io.codemc.bot.MockCodeMCBot; +import io.codemc.bot.MockJDA; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.InteractionType; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestAPIUtil { + + @BeforeAll + public static void ping() { + MockCodeMCBot.INSTANCE.validateConfig(); + + assertTrue(NexusAPI.ping()); + assertTrue(JenkinsAPI.ping()); + } + + @Test + @DisplayName("Test APIUtil#newPassword") + public void testCreatePassword() { + String p1 = APIUtil.newPassword(); + assertEquals(APIUtil.PASSWORD_SIZE, p1.length()); + + String p2 = APIUtil.newPassword(); + assertEquals(APIUtil.PASSWORD_SIZE, p2.length()); + assertNotEquals(p1, p2); + } + + @Test + @DisplayName("Test APIUtil#isGroup") + public void testIsGroup() { + // Tests if the username is a GitHub Organization + assertTrue(APIUtil.isGroup("CodeMC")); + assertTrue(APIUtil.isGroup("Team-Inceptus")); + + assertFalse(APIUtil.isGroup("gmitch215")); + assertFalse(APIUtil.isGroup("sgdc3")); + assertFalse(APIUtil.isGroup("Andre601")); + + assertFalse(APIUtil.isGroup("_")); + assertFalse(APIUtil.isGroup("-1")); + } + + @Test + @DisplayName("Test APIUtil#createNexus") + public void testNexus() { + String user1 = "TestNexus1"; + Member u1 = MockJDA.mockMember(user1); + String p1 = APIUtil.newPassword(); + InteractionHook h1 = MockJDA.mockInteractionHook(u1, MockJDA.REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + + assertTrue(APIUtil.createNexus(h1, user1, p1)); + assertTrue(NexusAPI.exists(user1)); + assertNotNull(NexusAPI.getNexusRepository(user1)); + + assertTrue(NexusAPI.deleteNexus(user1)); + assertFalse(NexusAPI.exists(user1)); + assertNull(NexusAPI.getNexusRepository(user1)); + + String user2 = "TestNexus2"; + Member u2 = MockJDA.mockMember(user2); + String p2 = APIUtil.newPassword(); + InteractionHook h2 = MockJDA.mockInteractionHook(u2, MockJDA.REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + + assertTrue(APIUtil.createNexus(h2, user2, p2)); + assertTrue(NexusAPI.exists(user2)); + assertNotNull(NexusAPI.getNexusRepository(user2)); + + assertTrue(NexusAPI.deleteNexus(user2)); + assertFalse(NexusAPI.exists(user2)); + assertNull(NexusAPI.getNexusRepository(user2)); + } + + @Test + @DisplayName("Test APIUtil#createJenkinsJob") + public void testJenkins() { + String user1 = "TestJenkins1"; + String j1 = "Job"; + Member u1 = MockJDA.mockMember(user1); + String p1 = APIUtil.newPassword(); + InteractionHook h1 = MockJDA.mockInteractionHook(u1, MockJDA.REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + + assertTrue(APIUtil.createJenkinsJob(h1, user1, p1, j1, "https://github.com/gmitch215/SocketMC", false)); + assertTrue(JenkinsAPI.existsUser(user1)); + assertNotNull(JenkinsAPI.getJobInfo(user1, j1)); + + assertTrue(JenkinsAPI.deleteJob(user1, j1)); + assertNull(JenkinsAPI.getJobInfo(user1, j1)); + assertTrue(JenkinsAPI.deleteUser(user1)); + assertFalse(JenkinsAPI.existsUser(user1)); + + String user2 = "TestJenkins2"; + String j2 = "Job"; + Member u2 = MockJDA.mockMember(user2); + String p2 = APIUtil.newPassword(); + InteractionHook h2 = MockJDA.mockInteractionHook(u2, MockJDA.REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + + assertTrue(APIUtil.createJenkinsJob(h2, user2, p2, j2, "https://github.com/CodeMC/Bot", false)); + assertTrue(JenkinsAPI.existsUser(user2)); + assertNotNull(JenkinsAPI.getJobInfo(user2, j2)); + + assertTrue(JenkinsAPI.deleteJob(user2, j2)); + assertNull(JenkinsAPI.getJobInfo(user2, j2)); + assertTrue(JenkinsAPI.deleteUser(user2)); + assertFalse(JenkinsAPI.existsUser(user2)); + + String user3 = "TestJenkins3"; + String j3 = "Job"; + Member u3 = MockJDA.mockMember(user3); + String p3 = APIUtil.newPassword(); + InteractionHook h3 = MockJDA.mockInteractionHook(u3, MockJDA.REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + + JenkinsAPI.createJenkinsUser(user3, p3, false); + APIUtil.createJenkinsJob(h3, user3, p3, j3, "https://github.com/CodeMC/Bot", false); + + MockJDA.assertEmbeds( + List.of(CommandUtil.embedError("Jenkins User for " + user3 + " already exists!")), + MockJDA.getEmbeds(h3.getIdLong()), + true + ); + + assertTrue(JenkinsAPI.deleteUser(user3)); + } + + @Test + @DisplayName("Test APIUtil#changePassword") + public void testChangePassword() { + String u1 = "TestChangePassword1"; + String j1 = "Job"; + Member user1 = MockJDA.mockMember(u1); + InteractionHook h1 = MockJDA.mockInteractionHook(user1, MockJDA.REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + + String old1 = APIUtil.newPassword(); + assertTrue(APIUtil.createNexus(h1, u1, old1)); + assertTrue(APIUtil.createJenkinsJob(h1, u1, old1, j1, "https://github.com/CodeMC/API", false)); + + String new1 = APIUtil.newPassword(); + assertTrue(APIUtil.changePassword(h1, u1, new1)); + + assertTrue(JenkinsAPI.deleteJob(u1, j1)); + assertTrue(JenkinsAPI.deleteUser(u1)); + assertTrue(NexusAPI.deleteNexus(u1)); + + String u2 = "TestChangePassword2"; + Member user2 = MockJDA.mockMember(u2); + InteractionHook h2 = MockJDA.mockInteractionHook(user2, MockJDA.REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + + String old2 = APIUtil.newPassword(); + assertTrue(APIUtil.createNexus(h2, u2, old2)); + + String new2 = APIUtil.newPassword(); + APIUtil.changePassword(h2, u2, new2); + + MockJDA.assertEmbeds( + List.of(CommandUtil.embedError("Failed to change Jenkins Password for " + u2 + "!")), + MockJDA.getEmbeds(h2.getIdLong()), + false + ); + + assertTrue(NexusAPI.deleteNexus(u2)); + } + +} diff --git a/src/test/java/io/codemc/bot/utils/TestApplicationHandler.java b/src/test/java/io/codemc/bot/utils/TestApplicationHandler.java new file mode 100644 index 0000000..8931908 --- /dev/null +++ b/src/test/java/io/codemc/bot/utils/TestApplicationHandler.java @@ -0,0 +1,141 @@ +package io.codemc.bot.utils; + +import io.codemc.api.database.DatabaseAPI; +import io.codemc.api.jenkins.JenkinsAPI; +import io.codemc.api.nexus.NexusAPI; +import io.codemc.bot.MockCodeMCBot; +import io.codemc.bot.MockJDA; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.InteractionType; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static io.codemc.bot.MockJDA.*; +import static org.junit.jupiter.api.Assertions.*; + +public class TestApplicationHandler { + + @Test + @DisplayName("Test ApplicationHandler#handle (Accepted)") + public void testHandleAccepted() { + String username = "TestApplicationHandlerAccepted"; + Member member = MockJDA.mockMember(username); + InteractionHook hook = MockJDA.mockInteractionHook(SELF, REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + + MessageEmbed embed = CommandUtil.requestEmbed("[" + username + "](userLink)", "[Job](repoLink)", member.getAsMention(), "description", member.getId()); + Message message = MockJDA.mockMessage("", List.of(embed), REQUEST_CHANNEL); + + assertFalse(CommandUtil.hasRole(member, List.of(AUTHOR.getIdLong()))); + assertFalse(JenkinsAPI.existsUser(username)); + assertFalse(NexusAPI.exists(username)); + assertNull(DatabaseAPI.getUser(username)); + + ApplicationHandler.handle( + MockCodeMCBot.INSTANCE, hook, GUILD, message.getIdLong(), null, true + ); + + String jenkinsUrl = MockCodeMCBot.INSTANCE.getConfigHandler().getString("jenkins", "url") + "/job/" + username + "/job/Job/"; + MessageCreateData expected = ApplicationHandler.getMessage(MockCodeMCBot.INSTANCE, member.getId(), "userLink", "repoLink", jenkinsUrl, SELF.getUser(), true); + String content = MockJDA.getLatestMessage(ACCEPTED_CHANNEL); + List embeds = MockJDA.getLatestEmbeds(ACCEPTED_CHANNEL); + + assertEquals(expected.getContent(), content); + assertEmbeds(expected.getEmbeds(), embeds, true); + + assertTrue(CommandUtil.hasRole(member, List.of(AUTHOR.getIdLong()))); + assertTrue(JenkinsAPI.existsUser(username)); + assertTrue(NexusAPI.exists(username)); + assertNotNull(DatabaseAPI.getUser(username)); + assertEquals(member.getIdLong(), DatabaseAPI.getUser(username).getDiscord()); + + assertTrue(JenkinsAPI.deleteUser(username)); + assertTrue(NexusAPI.deleteNexus(username)); + assertEquals(1, DatabaseAPI.removeUser(username)); + } + + @Test + @DisplayName("Test ApplicationHandler#handle (Rejected)") + public void testHandleRejected() { + String username = "TestApplicationHandlerRejected"; + Member member = MockJDA.mockMember(username); + InteractionHook hook = MockJDA.mockInteractionHook(SELF, REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + + MessageEmbed embed = CommandUtil.requestEmbed("[" + username + "](userLink)", "[Job](repoLink)", member.getAsMention(), "description", member.getId()); + Message message = MockJDA.mockMessage("", List.of(embed), REQUEST_CHANNEL); + + assertFalse(CommandUtil.hasRole(member, List.of(AUTHOR.getIdLong()))); + assertFalse(JenkinsAPI.existsUser(username)); + assertFalse(NexusAPI.exists(username)); + assertNull(DatabaseAPI.getUser(username)); + + ApplicationHandler.handle( + MockCodeMCBot.INSTANCE, hook, GUILD, message.getIdLong(), "Denied", false + ); + + MessageCreateData expected = ApplicationHandler.getMessage(MockCodeMCBot.INSTANCE, member.getId(), "userLink", "repoLink", "Denied", SELF.getUser(), false); + String content = MockJDA.getLatestMessage(REJECTED_CHANNEL); + List embeds = MockJDA.getLatestEmbeds(REJECTED_CHANNEL); + + assertEquals(expected.getContent(), content); + assertEmbeds(expected.getEmbeds(), embeds, true); + + assertFalse(CommandUtil.hasRole(member, List.of(AUTHOR.getIdLong()))); + assertFalse(JenkinsAPI.existsUser(username)); + assertFalse(NexusAPI.exists(username)); + assertNull(DatabaseAPI.getUser(username)); + } + + @Test + @DisplayName("Test ApplicationHandler#handle (Errors)") + public void testHandleErrors() { + InteractionHook h1 = MockJDA.mockInteractionHook(SELF, REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + Message m1 = MockJDA.mockMessage("", List.of(), REQUEST_CHANNEL); + + ApplicationHandler.handle( + MockCodeMCBot.INSTANCE, h1, GUILD, m1.getIdLong(), null, true + ); + assertEmbeds( + List.of(CommandUtil.embedError("Provided Message does not have any embeds.")), MockJDA.getEmbeds(h1.getIdLong()), true + ); + + InteractionHook h2 = MockJDA.mockInteractionHook(SELF, REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + MessageEmbed e2 = CommandUtil.getEmbed().build(); + Message m2 = MockJDA.mockMessage("", List.of(e2), REQUEST_CHANNEL); + + ApplicationHandler.handle( + MockCodeMCBot.INSTANCE, h2, GUILD, m2.getIdLong(), null, true + ); + assertEmbeds( + List.of(CommandUtil.embedError("Embed does not have a Footer or any Embed Fields")), MockJDA.getEmbeds(h2.getIdLong()), true + ); + + InteractionHook h3 = MockJDA.mockInteractionHook(SELF, REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + MessageEmbed e3 = CommandUtil.getEmbed().setFooter(" ").addField("null", "null", true).build(); + Message m3 = MockJDA.mockMessage("", List.of(e3), REQUEST_CHANNEL); + + ApplicationHandler.handle( + MockCodeMCBot.INSTANCE, h3, GUILD, m3.getIdLong(), null, true + ); + assertEmbeds( + List.of(CommandUtil.embedError("Embed does not have a valid footer.")), MockJDA.getEmbeds(h3.getIdLong()), true + ); + + InteractionHook h4 = MockJDA.mockInteractionHook(SELF, REQUEST_CHANNEL, InteractionType.MODAL_SUBMIT); + MessageEmbed e4 = CommandUtil.getEmbed().setFooter("id").addField("null", "null", true).build(); + Message m4 = MockJDA.mockMessage("", List.of(e4), REQUEST_CHANNEL); + + ApplicationHandler.handle( + MockCodeMCBot.INSTANCE, h4, GUILD, m4.getIdLong(), null, true + ); + assertEmbeds( + List.of(CommandUtil.embedError("Embed does not have all valid Fields.")), MockJDA.getEmbeds(h4.getIdLong()), true + ); + } + +} diff --git a/src/test/java/io/codemc/bot/utils/TestCommandUtil.java b/src/test/java/io/codemc/bot/utils/TestCommandUtil.java new file mode 100644 index 0000000..f041eaf --- /dev/null +++ b/src/test/java/io/codemc/bot/utils/TestCommandUtil.java @@ -0,0 +1,77 @@ +package io.codemc.bot.utils; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.codemc.bot.MockCodeMCBot; +import io.codemc.bot.MockJDA; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.InteractionType; + +public class TestCommandUtil { + + private static final long AUTHOR_ROLE = MockCodeMCBot.INSTANCE.getConfigHandler().getLong("author_role"); + private static final long ADMIN_ROLE = 405917902865170453L; + private static final long MAINTAINER_ROLE = 659568973079379971L; + + @Test + @DisplayName("Test CommandUtil#hasRole") + public void testHasRole() { + Member m1 = MockJDA.mockMember("gmitch215"); + + assertFalse(CommandUtil.hasRole(m1, List.of(AUTHOR_ROLE))); + + MockJDA.GUILD.addRoleToMember(m1, MockJDA.AUTHOR); + + assertTrue(CommandUtil.hasRole(m1, List.of(AUTHOR_ROLE))); + + Member m2 = MockJDA.mockMember("sgdc3"); + + assertFalse(CommandUtil.hasRole(m2, List.of(ADMIN_ROLE))); + assertFalse(CommandUtil.hasRole(m2, List.of(MAINTAINER_ROLE))); + + MockJDA.GUILD.addRoleToMember(m2, MockJDA.ADMINISTRATOR); + MockJDA.GUILD.addRoleToMember(m2, MockJDA.MAINTAINER); + + assertTrue(CommandUtil.hasRole(m2, List.of(ADMIN_ROLE, MAINTAINER_ROLE))); + } + + @Test + @DisplayName("Test CommandUtil.EmbedReply#build") + public void testEmbedReply() { + CommandUtil.EmbedReply r1 = CommandUtil.EmbedReply.empty(); + MessageEmbed m1 = r1.success("Success!").build(); + + MockJDA.assertEmbed(m1, CommandUtil.embedSuccess("Success!"), true); + + CommandUtil.EmbedReply r2 = CommandUtil.EmbedReply.empty(); + MessageEmbed m2 = r2.error("Error!").build(); + + MockJDA.assertEmbed(m2, CommandUtil.embedError("Error!"), true); + } + + @Test + @DisplayName("Test CommandUtil.EmbedReply#send") + public void testEmbedSend() { + Member member = MockJDA.mockMember("gmitch215"); + InteractionHook h1 = MockJDA.mockInteractionHook(member, MockJDA.REQUEST_CHANNEL, InteractionType.COMMAND); + CommandUtil.EmbedReply r1 = CommandUtil.EmbedReply.from(h1); + r1.success("Success!").send(); + MockJDA.assertEmbeds( + List.of(CommandUtil.embedSuccess("Success!")), + MockJDA.getEmbeds(h1.getIdLong()), + true + ); + + CommandUtil.EmbedReply r2 = CommandUtil.EmbedReply.empty(); + r2.error("Error!").send(); + } + +} diff --git a/src/test/resources/config.json b/src/test/resources/config.json new file mode 100644 index 0000000..cadb3cf --- /dev/null +++ b/src/test/resources/config.json @@ -0,0 +1,84 @@ +{ + "bot_token": "TOKEN", + "server": 405915656039694336, + "channels": { + "request_access": 1233971297185431582, + "accepted_requests": 784119059138478080, + "rejected_requests": 800423355551449098 + }, + "author_role": 405918641859723294, + "allowed_roles": { + "applications": { + "accept": [ + 405917902865170453, + 659568973079379971, + 1233971297185431582 + ], + "deny": [ + 405917902865170453, + 659568973079379971, + 1233971297185431582 + ] + }, + "commands": { + "application": [ + 405917902865170453, + 659568973079379971, + 1233971297185431582 + ], + "codemc": [ + 405917902865170453, + 659568973079379971 + ], + "disable": [ + 405917902865170453 + ], + "msg": [ + 405917902865170453 + ], + "reload": [ + 405917902865170453 + ] + } + }, + "users": { + "owner": 204232208049766400, + "co_owners": [ + 143088571656437760, + 282975975954710528 + ] + }, + "messages": { + "accepted": [ + "Your request has been **accepted**!", + "You will now be able to login with your GitHub Account and access the approved Repository on the CI.", + "", + "Remember to [visit our Documentation](https://docs.codemc.io) and [Read our FAQ](https://docs.codemc.io/faq) to learn how to setup automatic builds!" + ], + "denied": [ + "Your request has been **rejected**!", + "The reason for the denial is stated below.", + "", + "You may re-apply unless mentioned otherwise in the Reason." + ] + }, + "jenkins": { + "url": "http://localhost:8080", + "username": "admin", + "token": "00000000000000000000000000000000" + }, + "nexus": { + "url": "http://localhost:8081", + "username": "admin", + "password": "unset" + }, + "database": { + "service": "mariadb", + "host": "localhost", + "port": 3306, + "database": "test", + "username": "admin", + "password": "password" + }, + "github": "unset" +} \ No newline at end of file