diff --git a/common/src/main/java/me/lucko/luckperms/common/command/CommandManager.java b/common/src/main/java/me/lucko/luckperms/common/command/CommandManager.java index 1c83836f0..51cb7a2a1 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/CommandManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/CommandManager.java @@ -25,6 +25,7 @@ package me.lucko.luckperms.common.command; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ThreadFactoryBuilder; import me.lucko.luckperms.common.command.abstraction.Command; @@ -146,6 +147,11 @@ public class CommandManager { return this.tabCompletions; } + @VisibleForTesting + public Map> getMainCommands() { + return this.mainCommands; + } + public CompletableFuture executeCommand(Sender sender, String label, List args) { UUID uniqueId = sender.getUniqueId(); if (this.plugin.getConfiguration().get(ConfigKeys.COMMANDS_RATE_LIMIT) && !sender.isConsole() && !this.playerRateLimit.add(uniqueId)) { diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderShowTracks.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderShowTracks.java index d3e801d4a..fdfb363d7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderShowTracks.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderShowTracks.java @@ -36,14 +36,15 @@ import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.model.Track; +import me.lucko.luckperms.common.node.types.Inheritance; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; -import net.kyori.adventure.text.Component; import net.luckperms.api.node.Node; import net.luckperms.api.node.types.InheritanceNode; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; @@ -69,7 +70,7 @@ public class HolderShowTracks extends ChildCommand> lines = new ArrayList<>(); + List> lines = new ArrayList<>(); if (target.getType() == HolderType.USER) { // if the holder is a user, we want to query parent groups for tracks @@ -85,13 +86,7 @@ public class HolderShowTracks extends ChildCommand extends ChildCommand tracks = plugin.getTrackManager().getAll().values().stream() .filter(t -> t.containsGroup(groupName)) + .sorted(Comparator.comparing(Track::getName)) .collect(Collectors.toList()); for (Track track : tracks) { - lines.add(Maps.immutableEntry(track, Message.formatTrackPath(track.getGroups(), groupName))); + lines.add(Maps.immutableEntry(track, Inheritance.builder(groupName).build())); } } @@ -112,8 +108,10 @@ public class HolderShowTracks extends ChildCommand line : lines) { - Message.LIST_TRACKS_ENTRY.send(sender, line.getKey().getName(), line.getValue()); + for (Map.Entry line : lines) { + Track track = line.getKey(); + InheritanceNode node = line.getValue(); + Message.LIST_TRACKS_ENTRY.send(sender, track.getName(), node.getContexts(), Message.formatTrackPath(track.getGroups(), node.getGroupName())); } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ExportCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ExportCommand.java index f8ecab689..3d8de5476 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ExportCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ExportCommand.java @@ -122,4 +122,8 @@ public class ExportCommand extends SingleCommand { }); } + public boolean isRunning() { + return this.running.get(); + } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ImportCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ImportCommand.java index 9316ac02c..e0894120f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ImportCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ImportCommand.java @@ -142,4 +142,8 @@ public class ImportCommand extends SingleCommand { }); } + public boolean isRunning() { + return this.running.get(); + } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/locale/Message.java b/common/src/main/java/me/lucko/luckperms/common/locale/Message.java index 5a1ea2489..d192d9cbf 100644 --- a/common/src/main/java/me/lucko/luckperms/common/locale/Message.java +++ b/common/src/main/java/me/lucko/luckperms/common/locale/Message.java @@ -1938,13 +1938,25 @@ public interface Message { .append(text(':')) ); - Args2 LIST_TRACKS_ENTRY = (name, path) -> text() - // "&a{}: {}" - .color(GREEN) - .append(text(name)) - .append(text(": ")) - .append(path) - .build(); + Args3 LIST_TRACKS_ENTRY = (name, contextSet, path) -> join(newline(), + // "&3> &a{}: {}" + // "&7 ({}&7)" + text() + .append(text('>', DARK_AQUA)) + .append(space()) + .append(text().color(GREEN) + .append(text(name)) + .append(text(": ")) + .append(formatContextSetBracketed(contextSet, empty())) + .build() + ), + text() + .color(GRAY) + .append(text(" ")) + .append(OPEN_BRACKET) + .append(path) + .append(CLOSE_BRACKET) + ); Args1 LIST_TRACKS_EMPTY = holder -> prefixed(translatable() // "&b{}&a is not on any tracks." diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/SingletonPlayer.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/SingletonPlayer.java index cf6b64c8a..8d2453f74 100644 --- a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/SingletonPlayer.java +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/SingletonPlayer.java @@ -29,18 +29,35 @@ import me.lucko.luckperms.standalone.app.LuckPermsApplication; import me.lucko.luckperms.standalone.app.utils.AnsiUtils; import net.kyori.adventure.text.Component; +import java.util.Set; import java.util.UUID; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; /** * Dummy/singleton player class used by the standalone plugin. * - * In various places (ContextManager, SenderFactory, ..) the platform "player" type is used - * as a generic parameter. This class acts as this type for the standalone plugin. + *

In various places (ContextManager, SenderFactory, ..) the platform "player" type is used + * as a generic parameter. This class acts as this type for the standalone plugin.

*/ public class SingletonPlayer { + + /** Empty UUID used by the singleton player. */ + private static final UUID UUID = new UUID(0, 0); + + /** A message sink that prints the component to stdout */ + private static final Consumer PRINT_TO_STDOUT = component -> LuckPermsApplication.LOGGER.info(AnsiUtils.format(component)); + + /** Singleton instance */ public static final SingletonPlayer INSTANCE = new SingletonPlayer(); - private static final UUID UUID = new UUID(0, 0); + /** A set of message sinks that messages are delivered to */ + private final Set> messageSinks; + + private SingletonPlayer() { + this.messageSinks = new CopyOnWriteArraySet<>(); + this.messageSinks.add(PRINT_TO_STDOUT); + } public String getName() { return "StandaloneUser"; @@ -50,8 +67,18 @@ public class SingletonPlayer { return UUID; } - public void printStdout(Component component) { - LuckPermsApplication.LOGGER.info(AnsiUtils.format(component)); + public void sendMessage(Component component) { + for (Consumer sink : this.messageSinks) { + sink.accept(component); + } + } + + public void addMessageSink(Consumer sink) { + this.messageSinks.add(sink); + } + + public void removeMessageSink(Consumer sink) { + this.messageSinks.remove(sink); } } diff --git a/standalone/build.gradle b/standalone/build.gradle index c954a2aa9..7962117da 100644 --- a/standalone/build.gradle +++ b/standalone/build.gradle @@ -26,6 +26,7 @@ dependencies { testImplementation "org.testcontainers:junit-jupiter:1.17.6" testImplementation 'org.mockito:mockito-core:4.11.0' testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0' + testImplementation 'org.awaitility:awaitility:4.2.0' testImplementation 'com.zaxxer:HikariCP:4.0.3' testImplementation 'redis.clients:jedis:3.5.2' diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneSenderFactory.java b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneSenderFactory.java index 254dc50a3..ca21170a1 100644 --- a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneSenderFactory.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneSenderFactory.java @@ -53,7 +53,7 @@ public class StandaloneSenderFactory extends SenderFactory + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.standalone; + +import me.lucko.luckperms.standalone.app.integration.CommandExecutor; +import me.lucko.luckperms.standalone.utils.CommandTester; +import me.lucko.luckperms.standalone.utils.TestPluginProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.util.UUID; + +public class CommandsIntegrationTest { + + @Test + public void testGroupCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .givenCommand("creategroup test") + .thenExpect("[LP] test was successfully created.") + + .givenCommand("creategroup test2") + .thenExpect("[LP] test2 was successfully created.") + + .givenCommand("deletegroup test2") + .thenExpect("[LP] test2 was successfully deleted.") + + .givenCommand("listgroups") + .thenExpect(""" + [LP] Showing group entries: (page 1 of 1 - 2 entries) + [LP] Groups: (name, weight, tracks) + [LP] - default - 0 + [LP] - test - 0 + """ + ) + + .givenCommand("group test info") + .thenExpect(""" + [LP] > Group Info: test + [LP] - Display Name: test + [LP] - Weight: None + [LP] - Contextual Data: (mode: server) + [LP] Prefix: None + [LP] Suffix: None + [LP] Meta: None + """ + ) + + .givenCommand("group test meta set hello world") + .clearMessageBuffer() + + .givenCommand("group test setweight 10") + .thenExpect("[LP] Set weight to 10 for group test.") + + .givenCommand("group test setweight 100") + .thenExpect("[LP] Set weight to 100 for group test.") + + .givenCommand("group test setdisplayname Test") + .thenExpect("[LP] Set display name to Test for group test in context global.") + + .givenCommand("group test setdisplayname Dummy") + .thenExpect("[LP] Set display name to Dummy for group test in context global.") + + .givenCommand("group Dummy info") + .thenExpect(""" + [LP] > Group Info: test + [LP] - Display Name: Dummy + [LP] - Weight: 100 + [LP] - Contextual Data: (mode: server) + [LP] Prefix: None + [LP] Suffix: None + [LP] Meta: (weight=100) (hello=world) + """ + ) + + .givenCommand("group test clone testclone") + .thenExpect("[LP] test (Dummy) was successfully cloned onto testclone (Dummy).") + + .givenCommand("group testclone info") + .thenExpect(""" + [LP] > Group Info: testclone + [LP] - Display Name: Dummy + [LP] - Weight: 100 + [LP] - Contextual Data: (mode: server) + [LP] Prefix: None + [LP] Suffix: None + [LP] Meta: (weight=100) (hello=world) + """ + ) + + .givenCommand("group test rename test2") + .thenExpect("[LP] test (Dummy) was successfully renamed to test2 (Dummy).") + + .givenCommand("group test2 info") + .thenExpect(""" + [LP] > Group Info: test2 + [LP] - Display Name: Dummy + [LP] - Weight: 100 + [LP] - Contextual Data: (mode: server) + [LP] Prefix: None + [LP] Suffix: None + [LP] Meta: (weight=100) (hello=world) + """ + ) + + .givenCommand("listgroups") + .thenExpect(""" + [LP] Showing group entries: (page 1 of 1 - 3 entries) + [LP] Groups: (name, weight, tracks) + [LP] - test2 (Dummy) - 100 + [LP] - testclone (Dummy) - 100 + [LP] - default - 0 + """ + ); + }); + } + + @Test + public void testGroupPermissionCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .givenCommand("creategroup test") + .clearMessageBuffer() + + .givenCommand("group test permission set test.node true") + .thenExpect("[LP] Set test.node to true for test in context global.") + + .givenCommand("group test permission set test.node.other false server=test") + .thenExpect("[LP] Set test.node.other to false for test in context server=test.") + + .givenCommand("group test permission set test.node.other false server=test world=test2") + .thenExpect("[LP] Set test.node.other to false for test in context server=test, world=test2.") + + .givenCommand("group test permission settemp abc true 1h") + .thenExpect("[LP] Set abc to true for test for a duration of 1 hour in context global.") + + .givenCommand("group test permission settemp abc true 2h replace") + .thenExpect("[LP] Set abc to true for test for a duration of 2 hours in context global.") + + .givenCommand("group test permission unsettemp abc") + .thenExpect("[LP] Unset temporary permission abc for test in context global.") + + .givenCommand("group test permission info") + .thenExpect(""" + [LP] test's Permissions: (page 1 of 1 - 3 entries) + > test.node.other (server=test) (world=test2) + > test.node.other (server=test) + > test.node + """ + ) + + .givenCommand("group test permission unset test.node") + .thenExpect("[LP] Unset test.node for test in context global.") + + .givenCommand("group test permission unset test.node.other") + .thenExpect("[LP] test does not have test.node.other set in context global.") + + .givenCommand("group test permission unset test.node.other server=test") + .thenExpect("[LP] Unset test.node.other for test in context server=test.") + + .givenCommand("group test permission check test.node.other") + .thenExpect(""" + [LP] Permission information for test.node.other: + [LP] - test has test.node.other set to false in context server=test, world=test2. + [LP] - test does not inherit test.node.other. + [LP] + [LP] Permission check for test.node.other: + [LP] Result: undefined + [LP] Processor: None + [LP] Cause: None + [LP] Context: None + """ + ) + + .givenCommand("group test permission clear server=test world=test2") + .thenExpect("[LP] test's permissions were cleared in context server=test, world=test2. (1 node was removed.)") + + .givenCommand("group test permission info") + .thenExpect("[LP] test does not have any permissions set."); + }); + } + + @Test + public void testGroupParentCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .givenCommand("creategroup test") + .givenCommand("creategroup test2") + .givenCommand("creategroup test3") + .clearMessageBuffer() + + .givenCommand("group test parent add default") + .thenExpect("[LP] test now inherits permissions from default in context global.") + + .givenCommand("group test parent add test2 server=test") + .thenExpect("[LP] test now inherits permissions from test2 in context server=test.") + + .givenCommand("group test parent add test3 server=test") + .thenExpect("[LP] test now inherits permissions from test3 in context server=test.") + + .givenCommand("group test parent addtemp test2 1d server=hello") + .thenExpect("[LP] test now inherits permissions from test2 for a duration of 1 day in context server=hello.") + + .givenCommand("group test parent removetemp test2 server=hello") + .thenExpect("[LP] test no longer temporarily inherits permissions from test2 in context server=hello.") + + .givenCommand("group test parent info") + .thenExpect(""" + [LP] test's Parents: (page 1 of 1 - 3 entries) + > test2 (server=test) + > test3 (server=test) + > default + """ + ) + + .givenCommand("group test parent set test2 server=test") + .thenExpect("[LP] test had their existing parent groups cleared, and now only inherits test2 in context server=test.") + + .givenCommand("group test parent remove test2 server=test") + .thenExpect("[LP] test no longer inherits permissions from test2 in context server=test.") + + .givenCommand("group test parent clear") + .thenExpect("[LP] test's parents were cleared in context global. (1 node was removed.)") + + .givenCommand("group test parent info") + .thenExpect("[LP] test does not have any parents defined."); + }); + } + + @Test + public void testGroupMetaCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .givenCommand("creategroup test") + .clearMessageBuffer() + + .givenCommand("group test meta info") + .thenExpect(""" + [LP] test has no prefixes. + [LP] test has no suffixes. + [LP] test has no meta. + """ + ) + + .givenCommand("group test meta set hello world") + .thenExpect("[LP] Set meta key 'hello' to 'world' for test in context global.") + + .givenCommand("group test meta set hello world2 server=test") + .thenExpect("[LP] Set meta key 'hello' to 'world2' for test in context server=test.") + + .givenCommand("group test meta addprefix 10 \"&ehello world\"") + .thenExpect("[LP] test had prefix 'hello world' set at a priority of 10 in context global.") + + .givenCommand("group test meta addsuffix 100 \"&ehi\"") + .thenExpect("[LP] test had suffix 'hi' set at a priority of 100 in context global.") + + .givenCommand("group test meta addsuffix 1 \"&6no\"") + .thenExpect("[LP] test had suffix 'no' set at a priority of 1 in context global.") + + .givenCommand("group test meta settemp abc xyz 1d server=hello") + .thenExpect("[LP] Set meta key 'abc' to 'xyz' for test for a duration of 1 day in context server=hello.") + + .givenCommand("group test meta addtempprefix 1000 abc 1d server=hello") + .thenExpect("[LP] test had prefix 'abc' set at a priority of 1000 for a duration of 1 day in context server=hello.") + + .givenCommand("group test meta addtempsuffix 1000 xyz 3d server=hello") + .thenExpect("[LP] test had suffix 'xyz' set at a priority of 1000 for a duration of 3 days in context server=hello.") + + .givenCommand("group test meta unsettemp abc server=hello") + .thenExpect("[LP] Unset temporary meta key 'abc' for test in context server=hello.") + + .givenCommand("group test meta removetempprefix 1000 abc server=hello") + .thenExpect("[LP] test had temporary prefix 'abc' at priority 1000 removed in context server=hello.") + + .givenCommand("group test meta removetempsuffix 1000 xyz server=hello") + .thenExpect("[LP] test had temporary suffix 'xyz' at priority 1000 removed in context server=hello.") + + .givenCommand("group test meta info") + .thenExpect(""" + [LP] test's Prefixes + [LP] -> 10 - 'hello world' (inherited from self) + [LP] test's Suffixes + [LP] -> 100 - 'hi' (inherited from self) + [LP] -> 1 - 'no' (inherited from self) + [LP] test's Meta + [LP] -> hello = 'world2' (inherited from self) (server=test) + [LP] -> hello = 'world' (inherited from self) + """ + ) + + .givenCommand("group test info") + .thenExpect(""" + [LP] > Group Info: test + [LP] - Display Name: test + [LP] - Weight: None + [LP] - Contextual Data: (mode: server) + [LP] Prefix: "hello world" + [LP] Suffix: "hi" + [LP] Meta: (hello=world) + """ + ) + + .givenCommand("group test meta unset hello") + .thenExpect("[LP] Unset meta key 'hello' for test in context global.") + + .givenCommand("group test meta unset hello server=test") + .thenExpect("[LP] Unset meta key 'hello' for test in context server=test.") + + .givenCommand("group test meta removeprefix 10") + .thenExpect("[LP] test had all prefixes at priority 10 removed in context global.") + + .givenCommand("group test meta removesuffix 100") + .thenExpect("[LP] test had all suffixes at priority 100 removed in context global.") + + .givenCommand("group test meta removesuffix 1") + .thenExpect("[LP] test had all suffixes at priority 1 removed in context global.") + + .givenCommand("group test meta info") + .thenExpect(""" + [LP] test has no prefixes. + [LP] test has no suffixes. + [LP] test has no meta. + """ + ); + }); + } + + @Test + public void testUserCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + plugin.getStorage().savePlayerData(UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"), "Notch").join(); + + new CommandTester(executor) + .givenCommand("user Luck info") + .thenExpect(""" + [LP] > User Info: luck + [LP] - UUID: c1d60c50-70b5-4722-8057-87767557e50d + [LP] (type: official) + [LP] - Status: Offline + [LP] - Parent Groups: + [LP] > default + [LP] - Contextual Data: (mode: server) + [LP] Contexts: None + [LP] Prefix: None + [LP] Suffix: None + [LP] Primary Group: default + [LP] Meta: (primarygroup=default) + """ + ) + + .givenCommand("user c1d60c50-70b5-4722-8057-87767557e50d info") + .thenExpect(""" + [LP] > User Info: luck + [LP] - UUID: c1d60c50-70b5-4722-8057-87767557e50d + [LP] (type: official) + [LP] - Status: Offline + [LP] - Parent Groups: + [LP] > default + [LP] - Contextual Data: (mode: server) + [LP] Contexts: None + [LP] Prefix: None + [LP] Suffix: None + [LP] Primary Group: default + [LP] Meta: (primarygroup=default) + """ + ) + + .givenCommand("creategroup admin") + .givenCommand("user Luck parent set admin") + .clearMessageBuffer() + + .givenCommand("user Luck clone Notch") + .thenExpect("[LP] luck was successfully cloned onto notch.") + + .givenCommand("user Notch parent info") + .thenExpect(""" + [LP] notch's Parents: (page 1 of 1 - 1 entries) + > admin + """ + ); + }); + } + + @Test + public void testUserPermissionCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + + new CommandTester(executor) + .givenCommand("user Luck permission set test.node true") + .thenExpect("[LP] Set test.node to true for luck in context global.") + + .givenCommand("user Luck permission set test.node.other false server=test") + .thenExpect("[LP] Set test.node.other to false for luck in context server=test.") + + .givenCommand("user Luck permission set test.node.other false server=test world=test2") + .thenExpect("[LP] Set test.node.other to false for luck in context server=test, world=test2.") + + .givenCommand("user Luck permission settemp abc true 1h") + .thenExpect("[LP] Set abc to true for luck for a duration of 1 hour in context global.") + + .givenCommand("user Luck permission settemp abc true 2h replace") + .thenExpect("[LP] Set abc to true for luck for a duration of 2 hours in context global.") + + .givenCommand("user Luck permission unsettemp abc") + .thenExpect("[LP] Unset temporary permission abc for luck in context global.") + + .givenCommand("user Luck permission info") + .thenExpect(""" + [LP] luck's Permissions: (page 1 of 1 - 3 entries) + > test.node.other (server=test) (world=test2) + > test.node.other (server=test) + > test.node + """ + ) + + .givenCommand("user Luck permission unset test.node") + .thenExpect("[LP] Unset test.node for luck in context global.") + + .givenCommand("user Luck permission unset test.node.other") + .thenExpect("[LP] luck does not have test.node.other set in context global.") + + .givenCommand("user Luck permission unset test.node.other server=test") + .thenExpect("[LP] Unset test.node.other for luck in context server=test.") + + .givenCommand("user Luck permission check test.node.other") + .thenExpect(""" + [LP] Permission information for test.node.other: + [LP] - luck has test.node.other set to false in context server=test, world=test2. + [LP] - luck does not inherit test.node.other. + [LP] + [LP] Permission check for test.node.other: + [LP] Result: undefined + [LP] Processor: None + [LP] Cause: None + [LP] Context: None + """ + ) + + .givenCommand("user Luck permission clear server=test world=test2") + .thenExpect("[LP] luck's permissions were cleared in context server=test, world=test2. (1 node was removed.)") + + .givenCommand("user Luck permission info") + .thenExpect("[LP] luck does not have any permissions set."); + }); + } + + @Test + public void testUserParentCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + + new CommandTester(executor) + .givenCommand("creategroup test2") + .givenCommand("creategroup test3") + .clearMessageBuffer() + + .givenCommand("user Luck parent add default") + .thenExpect("[LP] luck already inherits from default in context global.") + + .givenCommand("user Luck parent add test2 server=test") + .thenExpect("[LP] luck now inherits permissions from test2 in context server=test.") + + .givenCommand("user Luck parent add test3 server=test") + .thenExpect("[LP] luck now inherits permissions from test3 in context server=test.") + + .givenCommand("user Luck parent addtemp test2 1d server=hello") + .thenExpect("[LP] luck now inherits permissions from test2 for a duration of 1 day in context server=hello.") + + .givenCommand("user Luck parent removetemp test2 server=hello") + .thenExpect("[LP] luck no longer temporarily inherits permissions from test2 in context server=hello.") + + .givenCommand("user Luck parent info") + .thenExpect(""" + [LP] luck's Parents: (page 1 of 1 - 3 entries) + > test2 (server=test) + > test3 (server=test) + > default + """ + ) + + .givenCommand("user Luck parent set test2 server=test") + .thenExpect("[LP] luck had their existing parent groups cleared, and now only inherits test2 in context server=test.") + + .givenCommand("user Luck parent remove test2 server=test") + .thenExpect("[LP] luck no longer inherits permissions from test2 in context server=test.") + + .givenCommand("user Luck parent clear") + .thenExpect("[LP] luck's parents were cleared in context global. (0 nodes were removed.)") + + .givenCommand("user Luck parent info") + .thenExpect(""" + [LP] luck's Parents: (page 1 of 1 - 1 entries) + > default + """ + ); + }); + } + + @Test + public void testUserMetaCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + + new CommandTester(executor) + .givenCommand("user Luck meta info") + .thenExpect(""" + [LP] luck has no prefixes. + [LP] luck has no suffixes. + [LP] luck has no meta. + """ + ) + + .givenCommand("user Luck meta set hello world") + .thenExpect("[LP] Set meta key 'hello' to 'world' for luck in context global.") + + .givenCommand("user Luck meta set hello world2 server=test") + .thenExpect("[LP] Set meta key 'hello' to 'world2' for luck in context server=test.") + + .givenCommand("user Luck meta addprefix 10 \"&ehello world\"") + .thenExpect("[LP] luck had prefix 'hello world' set at a priority of 10 in context global.") + + .givenCommand("user Luck meta addsuffix 100 \"&ehi\"") + .thenExpect("[LP] luck had suffix 'hi' set at a priority of 100 in context global.") + + .givenCommand("user Luck meta addsuffix 1 \"&6no\"") + .thenExpect("[LP] luck had suffix 'no' set at a priority of 1 in context global.") + + .givenCommand("user Luck meta settemp abc xyz 1d server=hello") + .thenExpect("[LP] Set meta key 'abc' to 'xyz' for luck for a duration of 1 day in context server=hello.") + + .givenCommand("user Luck meta addtempprefix 1000 abc 1d server=hello") + .thenExpect("[LP] luck had prefix 'abc' set at a priority of 1000 for a duration of 1 day in context server=hello.") + + .givenCommand("user Luck meta addtempsuffix 1000 xyz 3d server=hello") + .thenExpect("[LP] luck had suffix 'xyz' set at a priority of 1000 for a duration of 3 days in context server=hello.") + + .givenCommand("user Luck meta unsettemp abc server=hello") + .thenExpect("[LP] Unset temporary meta key 'abc' for luck in context server=hello.") + + .givenCommand("user Luck meta removetempprefix 1000 abc server=hello") + .thenExpect("[LP] luck had temporary prefix 'abc' at priority 1000 removed in context server=hello.") + + .givenCommand("user Luck meta removetempsuffix 1000 xyz server=hello") + .thenExpect("[LP] luck had temporary suffix 'xyz' at priority 1000 removed in context server=hello.") + + .givenCommand("user Luck meta info") + .thenExpect(""" + [LP] luck's Prefixes + [LP] -> 10 - 'hello world' (inherited from self) + [LP] luck's Suffixes + [LP] -> 100 - 'hi' (inherited from self) + [LP] -> 1 - 'no' (inherited from self) + [LP] luck's Meta + [LP] -> hello = 'world2' (inherited from self) (server=test) + [LP] -> hello = 'world' (inherited from self) + """ + ) + + .givenCommand("user Luck info") + .thenExpect(""" + [LP] > User Info: luck + [LP] - UUID: c1d60c50-70b5-4722-8057-87767557e50d + [LP] (type: official) + [LP] - Status: Offline + [LP] - Parent Groups: + [LP] > default + [LP] - Contextual Data: (mode: server) + [LP] Contexts: None + [LP] Prefix: "hello world" + [LP] Suffix: "hi" + [LP] Primary Group: default + [LP] Meta: (hello=world) (primarygroup=default) + """ + ) + + .givenCommand("user Luck meta unset hello") + .thenExpect("[LP] Unset meta key 'hello' for luck in context global.") + + .givenCommand("user Luck meta unset hello server=test") + .thenExpect("[LP] Unset meta key 'hello' for luck in context server=test.") + + .givenCommand("user Luck meta removeprefix 10") + .thenExpect("[LP] luck had all prefixes at priority 10 removed in context global.") + + .givenCommand("user Luck meta removesuffix 100") + .thenExpect("[LP] luck had all suffixes at priority 100 removed in context global.") + + .givenCommand("user Luck meta removesuffix 1") + .thenExpect("[LP] luck had all suffixes at priority 1 removed in context global.") + + .givenCommand("user Luck meta info") + .thenExpect(""" + [LP] luck has no prefixes. + [LP] luck has no suffixes. + [LP] luck has no meta. + """ + ); + }); + } + + @Test + public void testTrackCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .givenCommand("createtrack test1") + .thenExpect("[LP] test1 was successfully created.") + + .givenCommand("createtrack test2") + .thenExpect("[LP] test2 was successfully created.") + + .givenCommand("listtracks") + .thenExpect("[LP] Tracks: test1, test2") + + .givenCommand("deletetrack test2") + .thenExpect("[LP] test2 was successfully deleted.") + + .givenCommand("creategroup aaa") + .givenCommand("creategroup bbb") + .givenCommand("creategroup ccc") + .clearMessageBuffer() + + .givenCommand("track test1 append bbb") + .thenExpect("[LP] Group bbb was appended to track test1.") + + .givenCommand("track test1 insert aaa 1") + .thenExpect(""" + [LP] Group aaa was inserted into track test1 at position 1. + [LP] aaa ---> bbb + """ + ) + + .givenCommand("track test1 insert ccc 3") + .thenExpect(""" + [LP] Group ccc was inserted into track test1 at position 3. + [LP] aaa ---> bbb ---> ccc + """ + ) + + .givenCommand("track test1 info") + .thenExpect(""" + [LP] > Showing Track: test1 + [LP] - Path: aaa ---> bbb ---> ccc + """ + ) + + .givenCommand("track test1 clone testclone") + .thenExpect("[LP] test1 was successfully cloned onto testclone.") + + .givenCommand("track testclone info") + .thenExpect(""" + [LP] > Showing Track: testclone + [LP] - Path: aaa ---> bbb ---> ccc + """ + ) + + .givenCommand("track test1 rename test2") + .thenExpect("[LP] test1 was successfully renamed to test2.") + + .givenCommand("listtracks") + .thenExpect("[LP] Tracks: test2, testclone") + + .givenCommand("track test2 info") + .thenExpect(""" + [LP] > Showing Track: test2 + [LP] - Path: aaa ---> bbb ---> ccc + """ + ) + + .givenCommand("group aaa showtracks") + .thenExpect(""" + [LP] aaa's Tracks: + > test2: + (aaa ---> bbb ---> ccc) + > testclone: + (aaa ---> bbb ---> ccc) + """ + ) + + .givenCommand("track test2 remove bbb") + .thenExpect(""" + [LP] Group bbb was removed from track test2. + [LP] aaa ---> ccc + """ + ) + + .givenCommand("track test2 clear") + .thenExpect("[LP] test2's groups track was cleared."); + }); + } + + @Test + public void testUserTrackCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + + new CommandTester(executor) + .givenCommand("createtrack staff") + .givenCommand("createtrack premium") + + .givenCommand("creategroup mod") + .givenCommand("creategroup admin") + + .givenCommand("creategroup vip") + .givenCommand("creategroup vip+") + .givenCommand("creategroup mvp") + .givenCommand("creategroup mvp+") + + .givenCommand("track staff append mod") + .givenCommand("track staff append admin") + .givenCommand("track premium append vip") + .givenCommand("track premium append vip+") + .givenCommand("track premium append mvp") + .givenCommand("track premium append mvp+") + + .clearMessageBuffer() + + .givenCommand("user Luck promote staff") + .thenExpect("[LP] luck isn't in any groups on staff, so they were added to the first group, mod in context global.") + + .givenCommand("user Luck promote staff") + .thenExpect(""" + [LP] Promoting luck along track staff from mod to admin in context global. + [LP] mod ---> admin + """ + ) + + .givenCommand("user Luck promote staff") + .thenExpect("[LP] The end of track staff was reached, unable to promote luck.") + + .givenCommand("user Luck demote staff") + .thenExpect(""" + [LP] Demoting luck along track staff from admin to mod in context global. + [LP] mod <--- admin + """ + ) + + .givenCommand("user Luck demote staff") + .thenExpect("[LP] The end of track staff was reached, so luck was removed from mod.") + + .givenCommand("user Luck demote staff") + .thenExpect("[LP] luck isn't already in any groups on staff.") + + .givenCommand("user Luck promote premium server=test1") + .thenExpect("[LP] luck isn't in any groups on premium, so they were added to the first group, vip in context server=test1.") + + .givenCommand("user Luck promote premium server=test2") + .thenExpect("[LP] luck isn't in any groups on premium, so they were added to the first group, vip in context server=test2.") + + .givenCommand("user Luck promote premium server=test1") + .thenExpect(""" + [LP] Promoting luck along track premium from vip to vip+ in context server=test1. + [LP] vip ---> vip+ ---> mvp ---> mvp+ + """ + ) + + .givenCommand("user Luck promote premium server=test2") + .thenExpect(""" + [LP] Promoting luck along track premium from vip to vip+ in context server=test2. + [LP] vip ---> vip+ ---> mvp ---> mvp+ + """ + ) + + .givenCommand("user Luck parent info") + .thenExpect(""" + [LP] luck's Parents: (page 1 of 1 - 3 entries) + > vip+ (server=test2) + > vip+ (server=test1) + > default + """ + ) + + .givenCommand("user Luck showtracks") + .thenExpect(""" + [LP] luck's Tracks: + > premium: (server=test2) + (vip ---> vip+ ---> mvp ---> mvp+) + > premium: (server=test1) + (vip ---> vip+ ---> mvp ---> mvp+) + """ + ) + + .givenCommand("user Luck demote premium server=test1") + .thenExpect(""" + [LP] Demoting luck along track premium from vip+ to vip in context server=test1. + [LP] vip <--- vip+ <--- mvp <--- mvp+ + """ + ) + + .givenCommand("user Luck demote premium server=test2") + .thenExpect(""" + [LP] Demoting luck along track premium from vip+ to vip in context server=test2. + [LP] vip <--- vip+ <--- mvp <--- mvp+ + """ + ) + + .givenCommand("user Luck demote premium server=test1") + .thenExpect("[LP] The end of track premium was reached, so luck was removed from vip.") + + .givenCommand("user Luck demote premium server=test2") + .thenExpect("[LP] The end of track premium was reached, so luck was removed from vip.") + + .givenCommand("user Luck parent info") + .thenExpect(""" + [LP] luck's Parents: (page 1 of 1 - 1 entries) + > default + """ + ); + }); + } + +} diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/ImportExportIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/ImportExportIntegrationTest.java new file mode 100644 index 000000000..2600033d5 --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/ImportExportIntegrationTest.java @@ -0,0 +1,121 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.standalone; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import me.lucko.luckperms.common.commands.misc.ExportCommand; +import me.lucko.luckperms.common.commands.misc.ImportCommand; +import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.model.Track; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.node.types.Inheritance; +import me.lucko.luckperms.common.node.types.Permission; +import me.lucko.luckperms.standalone.app.integration.CommandExecutor; +import me.lucko.luckperms.standalone.utils.TestPluginProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.testcontainers.shaded.com.google.common.collect.ImmutableList; +import org.testcontainers.shaded.com.google.common.collect.ImmutableSet; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ImportExportIntegrationTest { + + @Test + public void testRoundTrip(@TempDir Path tempDirA, @TempDir Path tempDirB) throws IOException { + Path path = tempDirA.resolve("testfile.json.gz"); + + // run an export on environment A + TestPluginProvider.use(tempDirA, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + + executor.execute("creategroup test").join(); + executor.execute("group test permission set test.permission true").join(); + executor.execute("createtrack test").join(); + executor.execute("track test append default").join(); + executor.execute("user Luck permission set hello").join(); + + executor.execute("export testfile").join(); + + ExportCommand exportCommand = (ExportCommand) plugin.getCommandManager().getMainCommands().get("export"); + await().atMost(10, TimeUnit.SECONDS).until(() -> !exportCommand.isRunning()); + }); + + // check the export contains the expected data + try (BufferedReader reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(Files.newInputStream(path))))) { + JsonObject obj = new Gson().fromJson(reader, JsonObject.class); + assertEquals(2, obj.get("groups").getAsJsonObject().size()); + assertEquals(1, obj.get("users").getAsJsonObject().size()); + assertEquals(1, obj.get("tracks").getAsJsonObject().size()); + } + + // copy the export file from environment A to environment B + Files.copy(path, tempDirB.resolve("testfile.json.gz")); + + // import the file on environment B + TestPluginProvider.use(tempDirB, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + assertNull(plugin.getGroupManager().getIfLoaded("test")); + + executor.execute("import testfile").join(); + + ImportCommand importCommand = (ImportCommand) plugin.getCommandManager().getMainCommands().get("import"); + await().atMost(10, TimeUnit.SECONDS).until(() -> !importCommand.isRunning()); + + // assert that the expected objects exist + Group testGroup = plugin.getGroupManager().getIfLoaded("test"); + assertNotNull(testGroup); + assertEquals(ImmutableList.of(Permission.builder().permission("test.permission").build()), testGroup.normalData().asList()); + + Track testTrack = plugin.getTrackManager().getIfLoaded("test"); + assertNotNull(testTrack); + assertEquals(ImmutableList.of("default"), testTrack.getGroups()); + + User testUser = plugin.getStorage().loadUser(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), null).join(); + assertNotNull(testUser); + assertEquals("luck", testUser.getUsername().orElse(null)); + assertEquals(ImmutableSet.of( + Permission.builder().permission("hello").build(), + Inheritance.builder().group("default").build() + ), testUser.normalData().asSet()); + }); + } +} diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java new file mode 100644 index 000000000..b3a83e23e --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java @@ -0,0 +1,149 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.standalone.utils; + +import me.lucko.luckperms.standalone.app.integration.CommandExecutor; +import me.lucko.luckperms.standalone.app.integration.SingletonPlayer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Utility for testing LuckPerms commands with BDD-like given/when/then assertions. + */ +public final class CommandTester implements Consumer { + + private static final Logger LOGGER = LogManager.getLogger(CommandTester.class); + + /** The LuckPerms command executor */ + private final CommandExecutor executor; + + /** A buffer of messages received by the test tool */ + private final List messageBuffer = Collections.synchronizedList(new ArrayList<>()); + + public CommandTester(CommandExecutor executor) { + this.executor = executor; + } + + /** + * Accept a message and add it to the buffer. + * + * @param component the message + */ + @Override + public void accept(Component component) { + this.messageBuffer.add(component); + } + + /** + * Execute a command using the {@link CommandExecutor} and capture output to this test instance. + * + * @param command the command to run + * @return this + */ + public CommandTester givenCommand(String command) { + LOGGER.info("Executing test command: " + command); + + SingletonPlayer.INSTANCE.addMessageSink(this); + this.executor.execute(command).join(); + SingletonPlayer.INSTANCE.removeMessageSink(this); + + return this; + } + + /** + * Asserts that the current contents of the message buffer matches the given input string. + * + * @param expected the expected contents + * @return this + */ + public CommandTester thenExpect(String expected) { + String actual = this.renderBuffer().stream() + .map(String::trim) + .collect(Collectors.joining("\n")); + + assertEquals(expected.trim(), actual.trim()); + + return this.clearMessageBuffer(); + } + + /** + * Clears the message buffer. + * + * @return this + */ + public CommandTester clearMessageBuffer() { + this.messageBuffer.clear(); + return this; + } + + /** + * Renders the contents of the message buffer. + * + * @return rendered copy of the buffer + */ + public List renderBuffer() { + return this.messageBuffer.stream() + .map(component -> PlainTextComponentSerializer.plainText().serialize(component)) + .collect(Collectors.toList()); + } + + /** + * Prints test case source code to stdout to test the given command. + * + * @param cmd the command + * @return this + */ + public CommandTester outputTest(String cmd) { + System.out.printf(".executeCommand(\"%s\")%n", cmd); + this.givenCommand(cmd); + + List render = this.renderBuffer(); + if (render.size() == 1) { + System.out.printf(".expect(\"%s\")%n", render.get(0)); + } else { + System.out.println(".expect(\"\"\""); + for (String s : render) { + System.out.println(" " + s); + } + System.out.println(" \"\"\""); + System.out.println(")"); + } + + System.out.println(); + return this.clearMessageBuffer(); + } + +}