**Add unit tests for command framework preconditions**

Introduced comprehensive unit tests for precondition classes, including `DisabledCommandPrecondition`, `GuildOnlyPrecondition`, `SlashCommandOnlyPrecondition`, and `PermissionPrecondition`. Also added integration tests for `CommandManager` and `CommandProcessor` to validate functionality.
This commit is contained in:
anthorob committed 2026-10-24 05:39:06 -04:00
1 parent 8e4e77cd31
commit f96c2677b6
10 files changed
+1218 -21

No files matched your search

@@ -85,31 +85,33 @@ public class CommandManager implements CommandRegistry {
}
public void initCommands() {
Consumer<Command> onLoaded = command -> {
if (!command.isEnabled()) {
logger.info("Skipping command {} as it is disabled", command.getClass().getName());
return;
}
if (command.getCommandName() == null) {
logger.error("Failed to load command {}: Command name is null", command.getClass().getName());
return;
}
if (command.getDescription() == null && command.getType() == Command.Type.CHAT) {
logger.error("Failed to load command {}: Command description is null", command.getClass().getName());
return;
}
CommandContainer container = new CommandContainer(command);
if (!commands.containsKey(command.getCommandName())) {
commands.put(command.getCommandName(), container);
}
};
Consumer<Command> onLoaded = this::onCommandLoaded;
ReflectionHelper.loadCommands(Command.class, context, onLoaded);
}
void onCommandLoaded(Command command) {
if (!command.isEnabled()) {
logger.info("Skipping command {} as it is disabled", command.getClass().getName());
return;
}
if (command.getCommandName() == null) {
logger.error("Failed to load command {}: Command name is null", command.getClass().getName());
return;
}
if (command.getDescription() == null && command.getType() == Command.Type.CHAT) {
logger.error("Failed to load command {}: Command description is null", command.getClass().getName());
return;
}
CommandContainer container = new CommandContainer(command);
if (!commands.containsKey(command.getCommandName())) {
commands.put(command.getCommandName(), container);
}
}
@Override
public void registerSlashCommands(JDA jda) {
CommandListUpdateAction commandListUpdateAction = jda.updateCommands();
@@ -0,0 +1,199 @@
package me.bobert.netneutrality.core.command;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import java.util.function.Consumer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CommandContainerTest {
@Mock
private Command command;
@BeforeEach
void setup() {
when(command.getCommandName()).thenReturn("test");
when(command.getDescription()).thenReturn("A test command");
when(command.getType()).thenReturn(Command.Type.CHAT);
}
private CommandContainer container(Consumer<CommandOptionsBuilder> registration) {
doAnswer(invocation -> {
registration.accept(invocation.getArgument(0));
return null;
}).when(command).registerCommandOptions(org.mockito.ArgumentMatchers.any());
return new CommandContainer(command);
}
// ===== NULL_COMMAND and construction =====
@Test
void nullCommandShouldHaveNoCommand() {
assertNull(CommandContainer.NULL_COMMAND.getCommand());
}
@Test
void shouldExposeTheCommand() {
assertSame(command, container(_ -> { }).getCommand());
}
@Test
void shouldExposeTheBuiltSlashData() {
CommandContainer container = container(_ -> { });
assertNotNull(container.getSlashCommandData());
assertEquals("test", container.getSlashCommandData().getName());
}
@Test
void shouldExposeTheRegisteredOptionsSubcommandsAndGroups() {
CommandContainer container = container(builder -> {
builder.addOptions(new OptionData(OptionType.STRING, "text", "Text"));
});
assertEquals(1, container.getOptions().size());
assertTrue(container.getSubcommands().isEmpty());
assertTrue(container.getSubcommandGroups().isEmpty());
}
// ===== isEmpty =====
@Test
void shouldBeEmptyWithoutAnyOptions() {
assertTrue(container(_ -> { }).isEmpty());
}
@Test
void shouldNotBeEmptyWithOptions() {
assertFalse(container(builder -> builder.addOptions(new OptionData(OptionType.STRING, "text", "Text"))).isEmpty());
}
@Test
void shouldNotBeEmptyWithSubcommands() {
assertFalse(container(builder -> builder.addSubcommands(new SubcommandData("add", "Adds"))).isEmpty());
}
@Test
void shouldNotBeEmptyWithSubcommandGroups() {
CommandContainer container = container(builder -> builder.addSubcommandGroups(
new SubcommandGroupData("grp", "Group").addSubcommands(new SubcommandData("add", "Adds"))));
assertFalse(container.isEmpty());
}
// ===== printUsage =====
@Test
void shouldPrintNothingWithoutOptions() {
assertEquals("", container(_ -> { }).printUsage());
}
@Test
void shouldPrintPlainOptions() {
CommandContainer container = container(builder -> builder.addOptions(
new OptionData(OptionType.INTEGER, "amount", "Amount", true),
new OptionData(OptionType.STRING, "reason", "Reason")));
assertEquals("- Usage: /test <amount> <reason>\n"
+ " - *amount: Amount\n"
+ " - reason: Reason\n\n"
+ "-# *: required argument", container.printUsage());
}
@Test
void shouldPrintMinAndMaxValues() {
CommandContainer container = container(builder -> builder.addOptions(
new OptionData(OptionType.INTEGER, "amount", "Amount").setMinValue(1).setMaxValue(10)));
String usage = container.printUsage();
assertTrue(usage.contains("amount: Amount [Min value: 1] [Max value: 10]"), usage);
}
@Test
void shouldPrintMinAndMaxLengths() {
CommandContainer container = container(builder -> builder.addOptions(
new OptionData(OptionType.STRING, "text", "Text").setMinLength(2).setMaxLength(20)));
String usage = container.printUsage();
assertTrue(usage.contains("text: Text [Min length: 2] [Max length: 20]"), usage);
}
@Test
void shouldPrintChoices() {
CommandContainer container = container(builder -> builder.addOptions(
new OptionData(OptionType.STRING, "mode", "Mode").addChoice("One", "one").addChoice("Two", "two")));
String usage = container.printUsage();
assertTrue(usage.contains("[Available choices: one, two]"), usage);
}
@Test
void shouldPrintRequiredChannelTypes() {
CommandContainer container = container(builder -> builder.addOptions(
new OptionData(OptionType.CHANNEL, "channel", "Channel").setChannelTypes(ChannelType.TEXT)));
String usage = container.printUsage();
assertTrue(usage.contains("[Required channel types: TEXT]"), usage);
}
@Test
void shouldPrintSubcommands() {
CommandContainer container = container(builder -> builder.addSubcommands(
new SubcommandData("add", "Adds").addOptions(new OptionData(OptionType.STRING, "name", "Name", true))));
String usage = container.printUsage();
assertTrue(usage.contains("- Usage: /test add <name> (Adds)\n - *name: Name"), usage);
assertTrue(usage.endsWith("-# *: required argument"), usage);
}
@Test
void shouldPrintSubcommandGroups() {
CommandContainer container = container(builder -> builder.addSubcommandGroups(
new SubcommandGroupData("grp", "Group").addSubcommands(
new SubcommandData("add", "Adds").addOptions(new OptionData(OptionType.STRING, "name", "Name", true)))));
String usage = container.printUsage();
assertTrue(usage.contains("- Subcommand: /test grp (Group)\n"), usage);
assertTrue(usage.contains(" - Usage: /test grp add <name> (Adds)\n - *name: Name"), usage);
assertTrue(usage.endsWith("-# *: required argument"), usage);
}
@Test
void shouldNotPrintTheRequiredLegendForUsagelessCommands() {
assertFalse(container(_ -> { }).printUsage().contains("required argument"));
}
}
@@ -0,0 +1,138 @@
package me.bobert.netneutrality.core.command;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.tasks.TaskManager;
import me.bobert.netneutrality.db.service.GuildServiceProvider;
import me.bobert.netneutrality.core.command.precondition.DisabledCommandPrecondition;
import me.bobert.netneutrality.utils.UserFunction;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Dispatches through a real {@link CommandProcessor} with the default preconditions, usage validator and
* result handler, so only the Discord entities, the registry and the command itself are mocked.
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CommandDispatchIntegrationTest {
private static final String COMMAND_NAME = "test";
@Mock
private CommandContext context;
@Mock
private CommandRegistry commands;
@Mock
private TaskManager taskManager;
@Mock
private Command command;
@Mock
private Guild guild;
@Mock
private Member member;
@Mock
private User user;
@Mock
private MessageChannelUnion channel;
private CommandProcessor processor;
@BeforeEach
void setup() {
UserFunction.bind((GuildServiceProvider) null);
DisabledCommandPrecondition.bind(null);
when(command.getCommandName()).thenReturn(COMMAND_NAME);
when(command.getDescription()).thenReturn("A test command");
when(command.getType()).thenReturn(Command.Type.CHAT);
when(command.isEnabled()).thenReturn(true);
when(command.canBeExecutedFrom(channel)).thenReturn(true);
when(command.canBeExecutedBy(member)).thenReturn(true);
when(command.execute(context)).thenReturn(CommandExecutionResult.success("Executed"));
CommandContainer container = new CommandContainer(command);
when(commands.hasCommand(COMMAND_NAME)).thenReturn(true);
when(commands.getCommandContainer(COMMAND_NAME)).thenReturn(container);
when(context.parseCommandName()).thenReturn(COMMAND_NAME);
when(context.getFullCommand()).thenReturn(COMMAND_NAME);
when(context.getId()).thenReturn("context-id");
when(context.getGuild()).thenReturn(guild);
when(context.isFromGuild()).thenReturn(true);
when(context.getGuildUser()).thenReturn(member);
when(context.getDiscordUser()).thenReturn(user);
when(context.getChannel()).thenReturn(channel);
when(context.isValid()).thenReturn(true);
when(guild.getId()).thenReturn("111");
when(member.getId()).thenReturn("222");
when(user.getId()).thenReturn("222");
when(channel.getId()).thenReturn("333");
processor = new CommandProcessor(context, commands, taskManager);
}
@AfterEach
void tearDown() {
UserFunction.bind((GuildServiceProvider) null);
DisabledCommandPrecondition.bind(null);
}
@Test
void shouldBlockANonAdminOnAnAdminCommand() {
when(command.needAdmin()).thenReturn(true);
processor.dispatch();
verify(context).replyEphemeral("You are not powerful enough, scrub");
verify(command, never()).execute(any());
verify(context, never()).reply("Executed");
}
@Test
void shouldRunAnAllowedCommandToCompletion() {
processor.dispatch();
verify(context).setCommand(command);
verify(command).execute(context);
verify(context).reply("Executed");
}
@Test
void shouldBlockAGuildOnlyCommandInPrivateMessages() {
when(command.isGuildOnly()).thenReturn(true);
when(context.isFromGuild()).thenReturn(false);
processor.dispatch();
verify(context).replyEphemeral("This command is only available in a guild");
verify(context).deleteMessage();
verify(command, never()).execute(any());
}
}
@@ -0,0 +1,246 @@
package me.bobert.netneutrality.core.command;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.bootstrap.ApplicationContext;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.interactions.InteractionContextType;
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.RETURNS_SELF;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CommandManagerTest {
@Mock
private ApplicationContext applicationContext;
@Mock
private JDA jda;
private CommandListUpdateAction updateAction;
private CommandManager manager;
@BeforeEach
void setup() {
manager = new CommandManager(applicationContext);
updateAction = mock(CommandListUpdateAction.class, RETURNS_SELF);
when(jda.updateCommands()).thenReturn(updateAction);
}
private Command command(String name, String description) {
Command command = mock(Command.class);
when(command.getCommandName()).thenReturn(name);
when(command.getDescription()).thenReturn(description);
when(command.getType()).thenReturn(Command.Type.CHAT);
when(command.isEnabled()).thenReturn(true);
when(command.isSlashCommand()).thenReturn(true);
when(command.isGuildOnly()).thenReturn(false);
when(command.getSlashCommandPermission()).thenReturn(DefaultMemberPermissions.ENABLED);
return command;
}
@SuppressWarnings("unchecked")
private List<CommandData> registeredCommands() {
manager.registerSlashCommands(jda);
ArgumentCaptor<Collection<CommandData>> captor = ArgumentCaptor.forClass(Collection.class);
verify(updateAction).addCommands(captor.capture());
verify(updateAction).queue();
return List.copyOf(captor.getValue());
}
// ===== Construction =====
@Test
void shouldRequireTheApplicationContext() {
assertThrows(NullPointerException.class, () -> new CommandManager(null));
}
@Test
void shouldStartAfterTheOtherRegistries() {
assertEquals(30, manager.priority());
}
// ===== Loading =====
@Test
void shouldRegisterAnEnabledCommand() {
Command ping = command("ping", "Pings");
manager.onCommandLoaded(ping);
assertTrue(manager.hasCommand("ping"));
assertSame(ping, manager.getCommandContainer("ping").getCommand());
}
@Test
void shouldSkipDisabledCommands() {
Command ping = command("ping", "Pings");
when(ping.isEnabled()).thenReturn(false);
manager.onCommandLoaded(ping);
assertFalse(manager.hasCommand("ping"));
}
@Test
void shouldSkipCommandsWithoutAName() {
manager.onCommandLoaded(command(null, "Pings"));
assertTrue(manager.getCommands().isEmpty());
}
@Test
void shouldSkipChatCommandsWithoutADescription() {
manager.onCommandLoaded(command("ping", null));
assertFalse(manager.hasCommand("ping"));
}
@Test
void shouldKeepMessageCommandsWithoutADescription() {
Command menu = command("menu", null);
when(menu.getType()).thenReturn(Command.Type.MESSAGE);
manager.onCommandLoaded(menu);
assertTrue(manager.hasCommand("menu"));
}
@Test
void shouldKeepTheFirstCommandWhenNamesAreDuplicated() {
Command first = command("ping", "First");
Command second = command("ping", "Second");
manager.onCommandLoaded(first);
manager.onCommandLoaded(second);
assertEquals(1, manager.getCommands().size());
assertSame(first, manager.getCommandContainer("ping").getCommand());
}
// ===== Lookups =====
@Test
void shouldNotHaveAnUnknownCommand() {
assertFalse(manager.hasCommand("unknown"));
}
@Test
void shouldReturnTheNullCommandForAnUnknownName() {
assertSame(CommandContainer.NULL_COMMAND, manager.getCommandContainer("unknown"));
}
// ===== Slash command registration =====
@Test
void shouldRegisterEnabledSlashCommands() {
manager.onCommandLoaded(command("ping", "Pings"));
List<CommandData> registered = registeredCommands();
assertEquals(1, registered.size());
assertEquals("ping", registered.getFirst().getName());
}
@Test
void shouldRegisterNothingWithoutCommands() {
assertTrue(registeredCommands().isEmpty());
}
@Test
void shouldNotRegisterCommandsThatAreNotSlashCommands() {
Command ping = command("ping", "Pings");
manager.onCommandLoaded(ping);
when(ping.isSlashCommand()).thenReturn(false);
assertTrue(registeredCommands().isEmpty());
}
@Test
void shouldNotRegisterCommandsDisabledAfterLoading() {
Command ping = command("ping", "Pings");
manager.onCommandLoaded(ping);
when(ping.isEnabled()).thenReturn(false);
assertTrue(registeredCommands().isEmpty());
}
@Test
void shouldRestrictGuildOnlyCommandsToGuilds() {
Command ping = command("ping", "Pings");
when(ping.isGuildOnly()).thenReturn(true);
manager.onCommandLoaded(ping);
SlashCommandData data = (SlashCommandData) registeredCommands().getFirst();
assertEquals(Set.of(InteractionContextType.GUILD), data.getContexts());
}
@Test
void shouldAllowOtherCommandsInGuildsAndBotDms() {
manager.onCommandLoaded(command("ping", "Pings"));
SlashCommandData data = (SlashCommandData) registeredCommands().getFirst();
assertEquals(Set.of(InteractionContextType.GUILD, InteractionContextType.BOT_DM), data.getContexts());
}
@Test
void shouldApplyTheDefaultPermissionOfTheCommand() {
Command ping = command("ping", "Pings");
when(ping.getSlashCommandPermission()).thenReturn(DefaultMemberPermissions.DISABLED);
manager.onCommandLoaded(ping);
SlashCommandData data = (SlashCommandData) registeredCommands().getFirst();
assertEquals(DefaultMemberPermissions.DISABLED, data.getDefaultPermissions());
}
@Test
void shouldKeepRegisteringWhenOneCommandFails() {
Command broken = command("broken", "Broken");
CommandContainer brokenContainer = mock(CommandContainer.class);
when(brokenContainer.getCommand()).thenReturn(broken);
when(brokenContainer.getSlashCommandData()).thenThrow(new IllegalStateException("boom"));
manager.getCommands().put("broken", brokenContainer);
manager.onCommandLoaded(command("ping", "Pings"));
List<CommandData> registered = registeredCommands();
assertEquals(1, registered.size());
assertEquals("ping", registered.getFirst().getName());
verify(brokenContainer).getSlashCommandData();
}
}
@@ -0,0 +1,105 @@
package me.bobert.netneutrality.core.command;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CommandOptionsBuilderTest {
@Mock
private Command command;
@BeforeEach
void setup() {
when(command.getCommandName()).thenReturn("test");
when(command.getDescription()).thenReturn("A test command");
}
@Test
void shouldBuildSlashDataForChatCommands() {
CommandOptionsBuilder builder = new CommandOptionsBuilder(Command.Type.CHAT, command);
assertNotNull(builder.build());
assertEquals(net.dv8tion.jda.api.interactions.commands.Command.Type.SLASH, builder.getType());
assertEquals("test", builder.getName());
assertEquals("A test command", builder.getDescription());
}
@Test
void shouldBuildUserContextMenuDataForUserCommands() {
CommandOptionsBuilder builder = new CommandOptionsBuilder(Command.Type.USER, command);
assertEquals(net.dv8tion.jda.api.interactions.commands.Command.Type.USER, builder.build().getType());
assertEquals("test", builder.build().getName());
}
@Test
void shouldBuildMessageContextMenuDataForMessageCommands() {
CommandOptionsBuilder builder = new CommandOptionsBuilder(Command.Type.MESSAGE, command);
assertEquals(net.dv8tion.jda.api.interactions.commands.Command.Type.MESSAGE, builder.build().getType());
assertEquals("test", builder.build().getName());
}
@Test
void shouldStartWithoutOptions() {
CommandOptionsBuilder builder = new CommandOptionsBuilder(Command.Type.CHAT, command);
assertTrue(builder.getOptions().isEmpty());
assertTrue(builder.getSubcommands().isEmpty());
assertTrue(builder.getSubcommandGroups().isEmpty());
}
@Test
void shouldDelegateAddedOptions() {
CommandOptionsBuilder builder = new CommandOptionsBuilder(Command.Type.CHAT, command);
builder.addOptions(new OptionData(OptionType.STRING, "text", "Some text", true));
assertEquals(1, builder.build().getOptions().size());
assertEquals("text", builder.getOptions().getFirst().getName());
}
@Test
void shouldDelegateAddedSubcommandsAndGroups() {
CommandOptionsBuilder builder = new CommandOptionsBuilder(Command.Type.CHAT, command);
builder.addSubcommands(new SubcommandData("add", "Adds"));
builder.addSubcommandGroups(new SubcommandGroupData("grp", "Group").addSubcommands(new SubcommandData("sub", "Sub")));
assertEquals("add", builder.getSubcommands().getFirst().getName());
assertEquals("grp", builder.getSubcommandGroups().getFirst().getName());
}
@Test
void shouldDelegateSettersToTheBuiltData() {
CommandOptionsBuilder builder = new CommandOptionsBuilder(Command.Type.CHAT, command);
builder.setNSFW(true);
builder.setDescription("Changed");
assertTrue(builder.build().isNSFW());
assertEquals("Changed", builder.build().getDescription());
}
}
@@ -0,0 +1,113 @@
package me.bobert.netneutrality.core.command;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.core.tasks.Task;
import me.bobert.netneutrality.core.tasks.TaskManager;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CommandResultHandlerTest {
@Mock
private CommandContext context;
@Mock
private TaskManager taskManager;
@Mock
private Task task;
private final CommandResultHandler handler = new CommandResultHandler();
@Test
void shouldDoNothingForNoAction() {
handler.handle(context, CommandExecutionResult.none(), taskManager);
verify(context, never()).reply(anyString());
verifyNoInteractions(taskManager);
}
@Test
void shouldInsertTheTaskForATaskResult() {
handler.handle(context, CommandExecutionResult.task(task), taskManager);
verify(taskManager).insert(context, task);
}
@Test
void shouldNotReplyForATaskResultWithoutMessage() {
handler.handle(context, CommandExecutionResult.task(task), taskManager);
verify(context, never()).reply(anyString());
}
@Test
void shouldReplyForASuccess() {
handler.handle(context, CommandExecutionResult.success("Done"), taskManager);
verify(context).reply("Done");
verify(taskManager, never()).insert(any(), any());
}
@Test
void shouldReplyForAnInfo() {
handler.handle(context, CommandExecutionResult.info("FYI"), taskManager);
verify(context).reply("FYI");
}
@Test
void shouldReplyForAWarning() {
handler.handle(context, CommandExecutionResult.warn("Careful"), taskManager);
verify(context).reply("Careful");
}
@Test
void shouldReplyForAnError() {
handler.handle(context, CommandExecutionResult.error("Broken", new IllegalStateException("boom")), taskManager);
verify(context).reply("Broken");
}
@Test
void shouldReplyForAnErrorWithoutException() {
handler.handle(context, CommandExecutionResult.error("Broken"), taskManager);
verify(context).reply("Broken");
}
@Test
void shouldNotReplyWhenTheMessageIsNull() {
handler.handle(context, CommandExecutionResult.success((String) null), taskManager);
verify(context, never()).reply(anyString());
}
@Test
void shouldFailForANullResult() {
when(context.getId()).thenReturn("context-id");
assertThrows(RuntimeException.class, () -> handler.handle(context, null, taskManager));
}
}
@@ -0,0 +1,118 @@
package me.bobert.netneutrality.core.command.precondition;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.command.Command;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.db.GSettings;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.db.service.GuildServiceProvider;
import me.bobert.netneutrality.db.service.GuildSettingService;
import net.dv8tion.jda.api.entities.Guild;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class DisabledCommandPreconditionTest {
@Mock
private CommandContext context;
@Mock
private Command command;
@Mock
private Guild guild;
@Mock
private GuildServiceProvider provider;
@Mock
private GuildService guildService;
@Mock
private GuildSettingService settingService;
private final DisabledCommandPrecondition precondition = new DisabledCommandPrecondition();
@BeforeEach
void setup() {
when(context.getGuild()).thenReturn(guild);
when(guild.getId()).thenReturn("111");
when(command.getCommandName()).thenReturn("ping");
when(provider.get("111")).thenReturn(guildService);
when(guildService.getGuildSettingService()).thenReturn(settingService);
when(settingService.getOrDefault(GSettings.DISABLED_COMMANDS)).thenReturn("");
DisabledCommandPrecondition.bind(provider);
}
@AfterEach
void tearDown() {
DisabledCommandPrecondition.bind(null);
}
@Test
void shouldPassWhenNoProviderIsBound() {
DisabledCommandPrecondition.bind(null);
assertTrue(precondition.check(context, command));
}
@Test
void shouldPassOutsideAGuild() {
when(context.getGuild()).thenReturn(null);
assertTrue(precondition.check(context, command));
verify(context, never()).replyEphemeral(anyString());
}
@Test
void shouldPassWhenNoCommandIsDisabled() {
assertTrue(precondition.check(context, command));
verify(context, never()).replyEphemeral(anyString());
}
@Test
void shouldPassWhenAnotherCommandIsDisabled() {
when(settingService.getOrDefault(GSettings.DISABLED_COMMANDS)).thenReturn("play,skip");
assertTrue(precondition.check(context, command));
}
@Test
void shouldBlockADisabledCommand() {
when(settingService.getOrDefault(GSettings.DISABLED_COMMANDS)).thenReturn("play,ping");
assertFalse(precondition.check(context, command));
verify(context).replyEphemeral("This command is disabled on this server");
}
@Test
void shouldIgnoreCaseAndWhitespaceWhenMatching() {
when(settingService.getOrDefault(GSettings.DISABLED_COMMANDS)).thenReturn("play, PING ");
assertFalse(precondition.check(context, command));
}
}
@@ -0,0 +1,68 @@
package me.bobert.netneutrality.core.command.precondition;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.command.Command;
import me.bobert.netneutrality.core.context.CommandContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class GuildOnlyPreconditionTest {
@Mock
private CommandContext context;
@Mock
private Command command;
private final GuildOnlyPrecondition precondition = new GuildOnlyPrecondition();
@Test
void shouldPassGuildOnlyCommandInAGuild() {
when(command.isGuildOnly()).thenReturn(true);
when(context.isFromGuild()).thenReturn(true);
assertTrue(precondition.check(context, command));
verify(context, never()).replyEphemeral(org.mockito.ArgumentMatchers.anyString());
verify(context, never()).deleteMessage();
}
@Test
void shouldBlockGuildOnlyCommandOutsideAGuild() {
when(command.isGuildOnly()).thenReturn(true);
when(context.isFromGuild()).thenReturn(false);
assertFalse(precondition.check(context, command));
verify(context).replyEphemeral("This command is only available in a guild");
verify(context).deleteMessage();
}
@Test
void shouldPassCommandThatIsNotGuildOnlyOutsideAGuild() {
when(command.isGuildOnly()).thenReturn(false);
when(context.isFromGuild()).thenReturn(false);
assertTrue(precondition.check(context, command));
verify(context, never()).replyEphemeral(org.mockito.ArgumentMatchers.anyString());
verify(context, never()).deleteMessage();
}
}
@@ -0,0 +1,142 @@
package me.bobert.netneutrality.core.command.precondition;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.command.Command;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.utils.UserFunction;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import java.util.EnumSet;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class PermissionPreconditionTest {
private static final String DENIED = "You are not powerful enough, scrub";
@Mock
private CommandContext context;
@Mock
private Command command;
@Mock
private Member member;
@Mock
private Role role;
@Mock
private MessageChannelUnion channel;
private final PermissionPrecondition precondition = new PermissionPrecondition();
@BeforeEach
void setup() {
UserFunction.bind(null);
when(command.getType()).thenReturn(Command.Type.CHAT);
when(context.canSkipPermissionCheck()).thenReturn(false);
when(context.getChannel()).thenReturn(channel);
when(context.getGuildUser()).thenReturn(member);
when(command.canBeExecutedFrom(channel)).thenReturn(true);
when(command.needAdmin()).thenReturn(false);
when(command.canBeExecutedBy(member)).thenReturn(true);
when(member.getRoles()).thenReturn(List.of());
}
@AfterEach
void tearDown() {
UserFunction.bind(null);
}
@Test
void shouldSkipTheCheckForMessageCommands() {
when(command.getType()).thenReturn(Command.Type.MESSAGE);
when(command.canBeExecutedFrom(channel)).thenReturn(false);
when(command.canBeExecutedBy(member)).thenReturn(false);
assertTrue(precondition.check(context, command));
verify(context, never()).replyEphemeral(anyString());
}
@Test
void shouldPassWhenThePermissionCheckCanBeSkipped() {
when(context.canSkipPermissionCheck()).thenReturn(true);
when(command.canBeExecutedFrom(channel)).thenReturn(false);
when(command.canBeExecutedBy(member)).thenReturn(false);
assertTrue(precondition.check(context, command));
verify(context, never()).replyEphemeral(anyString());
}
@Test
void shouldBlockWhenTheCommandCannotRunInTheChannel() {
when(command.canBeExecutedFrom(channel)).thenReturn(false);
assertFalse(precondition.check(context, command));
verify(context).replyEphemeral("This command cannot be executed in the current channel");
}
@Test
void shouldBlockAnAdminCommandForANonAdmin() {
when(command.needAdmin()).thenReturn(true);
assertFalse(precondition.check(context, command));
verify(context).replyEphemeral(DENIED);
}
@Test
void shouldPassAnAdminCommandForAnAdmin() {
when(command.needAdmin()).thenReturn(true);
when(role.getPermissions()).thenReturn(EnumSet.of(Permission.ADMINISTRATOR));
when(member.getRoles()).thenReturn(List.of(role));
assertTrue(precondition.check(context, command));
verify(context, never()).replyEphemeral(anyString());
}
@Test
void shouldBlockWhenTheMemberCannotExecuteTheCommand() {
when(command.canBeExecutedBy(member)).thenReturn(false);
assertFalse(precondition.check(context, command));
verify(context).replyEphemeral(DENIED);
}
@Test
void shouldPassWhenEveryCheckSucceeds() {
assertTrue(precondition.check(context, command));
verify(context, never()).replyEphemeral(anyString());
}
}
@@ -0,0 +1,66 @@
package me.bobert.netneutrality.core.command.precondition;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import me.bobert.netneutrality.core.command.Command;
import me.bobert.netneutrality.core.context.CommandContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class SlashCommandOnlyPreconditionTest {
@Mock
private CommandContext context;
@Mock
private Command command;
private final SlashCommandOnlyPrecondition precondition = new SlashCommandOnlyPrecondition();
@Test
void shouldPassSlashOnlyCommandFromAnInteraction() {
when(command.isSlashCommandOnly()).thenReturn(true);
when(context.isInteraction()).thenReturn(true);
assertTrue(precondition.check(context, command));
verify(context, never()).replyEphemeral(anyString());
}
@Test
void shouldBlockSlashOnlyCommandSentAsAMessage() {
when(command.isSlashCommandOnly()).thenReturn(true);
when(context.isInteraction()).thenReturn(false);
assertFalse(precondition.check(context, command));
verify(context).replyEphemeral("This command is only available as a slash command");
}
@Test
void shouldPassCommandThatIsNotSlashOnlyAsAMessage() {
when(command.isSlashCommandOnly()).thenReturn(false);
when(context.isInteraction()).thenReturn(false);
assertTrue(precondition.check(context, command));
verify(context, never()).replyEphemeral(anyString());
}
}