package fr.xephi.authme.command; import fr.xephi.authme.util.StringUtils; import org.junit.BeforeClass; import org.junit.Test; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; /** * Test for {@link CommandManager}, especially to guarantee the integrity of the defined commands. */ public class CommandManagerTest { /** * Defines the maximum allowed depths for nesting CommandDescription instances. * Note that the depth starts at 0 (e.g. /authme), so a depth of 2 is something like /authme hello world */ private static int MAX_ALLOWED_DEPTH = 1; private static CommandManager manager; @BeforeClass public static void initializeCommandManager() { manager = new CommandManager(true); } @Test public void shouldInitializeCommands() { // given/when int commandCount = manager.getCommandDescriptionCount(); List commands = manager.getCommandDescriptions(); // then // It obviously doesn't make sense to test much of the concrete data // that is being initialized; we just want to guarantee with this test // that data is indeed being initialized and we take a few "probes" assertThat(commandCount, equalTo(9)); assertThat(commandsIncludeLabel(commands, "authme"), equalTo(true)); assertThat(commandsIncludeLabel(commands, "register"), equalTo(true)); assertThat(commandsIncludeLabel(commands, "help"), equalTo(false)); } @Test public void shouldNotBeNestedExcessively() { // given BiConsumer descriptionTester = new BiConsumer() { @Override public void accept(CommandDescription command, int depth) { assertThat(depth <= MAX_ALLOWED_DEPTH, equalTo(true)); } }; // when/then walkThroughCommands(manager.getCommandDescriptions(), descriptionTester); } /** Ensure that all children of a command stored the parent. */ @Test public void shouldHaveConnectionBetweenParentAndChild() { // given BiConsumer connectionTester = new BiConsumer() { @Override public void accept(CommandDescription command, int depth) { if (command.hasChildren()) { for (CommandDescription child : command.getChildren()) { assertThat(command.equals(child.getParent()), equalTo(true)); } } // Checking that the parent has the current command as child is redundant as this is how we can traverse // the "command tree" in the first place - if we're here, it's that the parent definitely has the // command as child. } }; // when/then walkThroughCommands(manager.getCommandDescriptions(), connectionTester); } @Test public void shouldNotDefineSameLabelTwice() { // given final Set commandMappings = new HashSet<>(); BiConsumer uniqueMappingTester = new BiConsumer() { @Override public void accept(CommandDescription command, int depth) { int initialSize = commandMappings.size(); List newMappings = getAbsoluteLabels(command); commandMappings.addAll(newMappings); // Set only contains unique entries, so we just check after adding all new mappings that the size // of the Set corresponds to our expectation assertThat("All bindings are unique for command with bindings '" + newMappings + "'", commandMappings.size() == initialSize + newMappings.size(), equalTo(true)); } }; // when/then walkThroughCommands(manager.getCommandDescriptions(), uniqueMappingTester); } /** * The description should provide a very short description of the command and shouldn't end in a ".", whereas the * detailed description should be longer and end with a period. */ @Test public void shouldHaveProperDescription() { // given BiConsumer descriptionTester = new BiConsumer() { @Override public void accept(CommandDescription command, int depth) { String forCommandText = " for command with labels '" + command.getLabels() + "'"; assertThat("has description" + forCommandText, StringUtils.isEmpty(command.getDescription()), equalTo(false)); assertThat("short description doesn't end in '.'" + forCommandText, command.getDescription().endsWith("."), equalTo(false)); assertThat("has detailed description" + forCommandText, StringUtils.isEmpty(command.getDetailedDescription()), equalTo(false)); assertThat("detailed description ends in '.'" + forCommandText, command.getDetailedDescription().endsWith("."), equalTo(true)); } }; // when/then walkThroughCommands(manager.getCommandDescriptions(), descriptionTester); } /** * Check that the implementation of {@link ExecutableCommand} a command points to is the same for each type: * it is inefficient to instantiate the same type multiple times. */ @Test public void shouldNotHaveMultipleInstancesOfSameExecutableCommandSubType() { // given final Map, ExecutableCommand> implementations = new HashMap<>(); CommandManager manager = new CommandManager(true); BiConsumer descriptionTester = new BiConsumer() { @Override public void accept(CommandDescription command, int depth) { assertThat(command.getExecutableCommand(), not(nullValue())); ExecutableCommand commandExec = command.getExecutableCommand(); ExecutableCommand storedExec = implementations.get(command.getExecutableCommand().getClass()); if (storedExec != null) { assertThat("has same implementation of '" + storedExec.getClass().getName() + "' for command with " + "parent " + (command.getParent() == null ? "null" : command.getParent().getLabels()), storedExec == commandExec, equalTo(true)); } else { implementations.put(commandExec.getClass(), commandExec); } } }; // when List commands = manager.getCommandDescriptions(); // then walkThroughCommands(commands, descriptionTester); } @Test public void shouldHaveOptionalArgumentsAfterMandatoryOnes() { // given BiConsumer argumentOrderTester = new BiConsumer() { @Override public void accept(CommandDescription command, int depth) { boolean encounteredOptionalArg = false; for (CommandArgumentDescription argument : command.getArguments()) { if (argument.isOptional()) { encounteredOptionalArg = true; } else if (!argument.isOptional() && encounteredOptionalArg) { fail("Mandatory arguments should come before optional ones for command with labels '" + command.getLabels() + "'"); } } } }; // when/then walkThroughCommands(manager.getCommandDescriptions(), argumentOrderTester); } /** * Ensure that a command with children (i.e. a base command) doesn't define any arguments. This might otherwise * clash with the label of the child. */ @Test public void shouldNotHaveArgumentsIfCommandHasChildren() { // given BiConsumer noArgumentForParentChecker = new BiConsumer() { @Override public void accept(CommandDescription command, int depth) { // Fail if the command has children and has arguments at the same time // Exception: If the parent only has one child defining the help label, it is acceptable if (command.hasChildren() && command.hasArguments() && (command.getChildren().size() != 1 || !command.getChildren().get(0).hasLabel("help"))) { fail("Parent command (labels='" + command.getLabels() + "') should not have any arguments"); } } }; // when/then walkThroughCommands(manager.getCommandDescriptions(), noArgumentForParentChecker); } // ------------ // Helper methods // ------------ private static void walkThroughCommands(List commands, BiConsumer consumer) { walkThroughCommands(commands, consumer, 0); } private static void walkThroughCommands(List commands, BiConsumer consumer, int depth) { for (CommandDescription command : commands) { consumer.accept(command, depth); if (command.hasChildren()) { walkThroughCommands(command.getChildren(), consumer, depth + 1); } } } private static boolean commandsIncludeLabel(Iterable commands, String label) { for (CommandDescription command : commands) { if (command.getLabels().contains(label)) { return true; } } return false; } private interface BiConsumer { void accept(CommandDescription command, int depth); } /** * Get the absolute label that a command defines. Note: Assumes that only the passed command might have * multiple labels; only considering the first label for all of the command's parents. * * @param command The command to verify * * @return The full command binding */ private static List getAbsoluteLabels(CommandDescription command) { String parentPath = ""; CommandDescription elem = command.getParent(); while (elem != null) { parentPath = elem.getLabels().get(0) + " " + parentPath; elem = elem.getParent(); } parentPath = parentPath.trim(); List bindings = new ArrayList<>(command.getLabels().size()); for (String label : command.getLabels()) { bindings.add(StringUtils.join(" ", parentPath, label)); } return bindings; } }