diff --git a/docs/Reference/Pantheon-CLI-Syntax.md b/docs/Reference/Pantheon-CLI-Syntax.md index fcdec696a3..1753293c28 100644 --- a/docs/Reference/Pantheon-CLI-Syntax.md +++ b/docs/Reference/Pantheon-CLI-Syntax.md @@ -1033,4 +1033,76 @@ $ pantheon password hash --password= ```bash tab="Example" $ pantheon password hash --password=myPassword123 -``` \ No newline at end of file +``` + +### rlp + +This command provides RLP related actions. + +#### encode + +This command encodes a typed JSON value from a file or from the standard input into an RLP hexadecimal string. + +```bash tab="Syntax" +$ pantheon rlp encode [--from=] [--to=] [--type=] +``` + +```bash tab="Example using files" +$ pantheon rlp encode --from=ibft_extra_data.json --to=extra_data_for_ibft_genesis.txt --type=IBFT_EXTRA_DATA +``` + +```bash tab="Example using standard input/output and default type" +$ cat extra_data.json | pantheon rlp encode > rlp.txt +``` + +##### Available types for encoding + +For the moment, only IBFT extra data type of RLP data is supported. +This data is used to build the IBFT 2.0 genesis file. + +???+ summary "IBFT_EXTRA_DATA" + To generate the IBFT_EXTRA_DATA typed RLP string you need a JSON input containing an array with + all the validator addresses strings that you want to insert in your genesis. + + The JSON content is an object with a validator property that's an array of validator addresse strings. + + ??? tip "JSON Schema for IBFT_EXTRA_DATA" + The following JSON Schema can be used to validate that your JSON data is well formed. + + Among many tools you can use some online validator tool like https://www.jsonschemavalidator.net/ + to validate your JSON content. + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://tech.pegasys.pantheon/cli_rlp_ibft_extra_data.json", + "type": "array", + "definitions": {}, + "title": "IBFT extra data", + "description":"JSON format used as input to generate an IBFT extra data RLP string", + "items": { + "$id": "#/address", + "type": "string", + "title": "Validator address", + "description":"The validator node address", + "default": "", + "examples": [ + "be068f726a13c8d46c44be6ce9d275600e1735a4", + "5ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193" + ], + "pattern":"^([0-9a-f]{40})$" + } + } + ``` + + !!!example "Example IBFT_EXTRA_DATA encoding" + ```json tab="JSON input" + [ + "be068f726a13c8d46c44be6ce9d275600e1735a4", + "5ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193" + ] + ``` + + ``` tab="RLP output" + 0xf853a00000000000000000000000000000000000000000000000000000000000000000ea94be068f726a13c8d46c44be6ce9d275600e1735a4945ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193808400000000c0 + ``` \ No newline at end of file diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java b/pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java index 5bb6c4ba00..436483e235 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java @@ -39,6 +39,7 @@ public static void main(final String... args) { pantheonCommand.parse( new RunLast().andExit(SUCCESS_EXIT_CODE), defaultExceptionHandler().andExit(ERROR_EXIT_CODE), + System.in, args); } } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/DefaultCommandValues.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/DefaultCommandValues.java index a605093a6d..e5a206008b 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/DefaultCommandValues.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/DefaultCommandValues.java @@ -26,7 +26,7 @@ import picocli.CommandLine; -interface DefaultCommandValues { +public interface DefaultCommandValues { String CONFIG_FILE_OPTION_NAME = "--config-file"; String MANDATORY_PATH_FORMAT_HELP = ""; diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java index cc049ad457..fb19c027a2 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -31,6 +31,7 @@ import tech.pegasys.pantheon.cli.custom.EnodeToURIPropertyConverter; import tech.pegasys.pantheon.cli.custom.JsonRPCWhitelistHostsProperty; import tech.pegasys.pantheon.cli.custom.RpcAuthFileValidator; +import tech.pegasys.pantheon.cli.rlp.RLPSubCommand; import tech.pegasys.pantheon.config.GenesisConfigFile; import tech.pegasys.pantheon.consensus.clique.jsonrpc.CliqueRpcApis; import tech.pegasys.pantheon.consensus.ibft.jsonrpc.IbftRpcApis; @@ -58,6 +59,7 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.InetAddress; import java.net.URI; import java.nio.file.Path; @@ -474,6 +476,7 @@ public PantheonCommand( public void parse( final AbstractParseResultHandler> resultHandler, final DefaultExceptionHandler> exceptionHandler, + final InputStream in, final String... args) { commandLine = new CommandLine(this); @@ -492,6 +495,8 @@ public void parse( PublicKeySubCommand.COMMAND_NAME, new PublicKeySubCommand(resultHandler.out())); commandLine.addSubcommand( PasswordSubCommand.COMMAND_NAME, new PasswordSubCommand(resultHandler.out())); + commandLine.addSubcommand( + RLPSubCommand.COMMAND_NAME, new RLPSubCommand(resultHandler.out(), in)); commandLine.registerConverter(Address.class, Address::fromHexString); commandLine.registerConverter(BytesValue.class, BytesValue::fromHexString); diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/IbftExtraDataCLIAdapter.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/IbftExtraDataCLIAdapter.java new file mode 100644 index 0000000000..9230d67600 --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/IbftExtraDataCLIAdapter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.cli.rlp; + +import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Adapter to convert a typed JSON to an IbftExtraData object This adapter handles the JSON to RLP + * encoding + */ +public class IbftExtraDataCLIAdapter implements JSONToRLP { + + @Override + public BytesValue encode(final String json) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + TypeReference> typeRef = new TypeReference>() {}; + Collection validatorAddresse = mapper.readValue(json, typeRef); + + Collection
addresses = + validatorAddresse.stream().map(Address::fromHexString).collect(Collectors.toList()); + + return new IbftExtraData( + BytesValue.wrap(new byte[32]), Collections.emptyList(), Optional.empty(), 0, addresses) + .encode(); + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/JSONToRLP.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/JSONToRLP.java new file mode 100644 index 0000000000..70696a8479 --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/JSONToRLP.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.cli.rlp; + +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.io.IOException; + +/** Behaviour of objects that can be encoded from JSON to RLP */ +interface JSONToRLP { + + /** + * Encodes the object into an RLP value. + * + * @param json the JSON to convert to RLP + * @return the RLP encoded object. + * @throws IOException if an error occurs while reading data + */ + BytesValue encode(String json) throws IOException; +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/RLPSubCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/RLPSubCommand.java new file mode 100644 index 0000000000..ebfbfd3716 --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/RLPSubCommand.java @@ -0,0 +1,200 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.cli.rlp; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.charset.StandardCharsets.UTF_8; +import static tech.pegasys.pantheon.cli.DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP; + +import tech.pegasys.pantheon.cli.PantheonCommand; +import tech.pegasys.pantheon.cli.rlp.RLPSubCommand.EncodeSubCommand; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Scanner; + +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import picocli.CommandLine.Command; +import picocli.CommandLine.ExecutionException; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.ParentCommand; +import picocli.CommandLine.Spec; + +@Command( + name = RLPSubCommand.COMMAND_NAME, + description = "This command provides RLP data related actions.", + mixinStandardHelpOptions = true, + subcommands = {EncodeSubCommand.class}) +public class RLPSubCommand implements Runnable { + + public static final String COMMAND_NAME = "rlp"; + private static final Logger LOG = LogManager.getLogger(); + + private final PrintStream out; + private final InputStream in; + + @SuppressWarnings("unused") + @ParentCommand + private PantheonCommand parentCommand; + + @SuppressWarnings("unused") + @Spec + private CommandSpec spec; + + public RLPSubCommand(final PrintStream out, final InputStream in) { + this.out = out; + this.in = in; + } + + @Override + public void run() { + spec.commandLine().usage(out); + } + + /** + * RLP encode sub-command + * + *

Encode a JSON data into an RLP hex string. + */ + @Command( + name = "encode", + description = "This command encodes a JSON typed data into an RLP hex string.", + mixinStandardHelpOptions = true) + static class EncodeSubCommand implements Runnable { + + @SuppressWarnings("unused") + @ParentCommand + private RLPSubCommand parentCommand; // Picocli injects reference to parent command + + @SuppressWarnings("unused") + @Spec + private CommandSpec spec; + + @Option( + names = "--type", + description = + "Type of the RLP data to encode, possible values are ${COMPLETION-CANDIDATES}. (default: ${DEFAULT-VALUE})", + arity = "1..1") + private RLPType type = RLPType.IBFT_EXTRA_DATA; + + @Option( + names = "--from", + paramLabel = MANDATORY_FILE_FORMAT_HELP, + description = "File containing JSON object to encode", + arity = "1..1") + private File jsonSourceFile = null; + + @Option( + names = "--to", + paramLabel = MANDATORY_FILE_FORMAT_HELP, + description = "File to write encoded RLP string to.", + arity = "1..1") + private File rlpTargetFile = null; + + @Override + public void run() { + checkNotNull(parentCommand); + readInput(); + } + + /** + * Reads the stdin or from a file if one is specified by {@link #jsonSourceFile} then goes to + * {@link #encode(String)} this data + */ + private void readInput() { + // if we have an output file defined, print to it + // otherwise print to defined output, usually standard output. + StringBuilder jsonData = new StringBuilder(); + + if (jsonSourceFile != null) { + try { + BufferedReader reader = Files.newBufferedReader(jsonSourceFile.toPath(), UTF_8); + + String line; + while ((line = reader.readLine()) != null) jsonData.append(line); + } catch (IOException e) { + throw new ExecutionException(spec.commandLine(), "Unable to read JSON file."); + } + } else { + // get JSON data from standard input + try (Scanner scanner = new Scanner(parentCommand.in, UTF_8.name())) { + while (scanner.hasNextLine()) { + jsonData.append(String.join("", scanner.nextLine().split("\\s"))); + } + } + } + + // next step is to encode the value + encode(jsonData.toString()); + } + + /** + * Encodes the JSON input into an RLP data based on the {@link #type} then goes to {@link + * #writeOutput(BytesValue)} this data to file or stdout + * + * @param jsonInput the JSON string data to encode + */ + private void encode(final String jsonInput) { + if (jsonInput == null || jsonInput.isEmpty()) { + throw new ParameterException( + spec.commandLine(), "An error occurred while trying to read the JSON data."); + } else { + try { + // encode and write the value + writeOutput(type.getAdapter().encode(jsonInput)); + } catch (MismatchedInputException e) { + throw new ParameterException( + spec.commandLine(), + "Unable to map the JSON data with selected type. Please check JSON input format. " + + e); + } catch (IOException e) { + throw new ParameterException( + spec.commandLine(), + "Unable to load the JSON data. Please check JSON input format. " + e); + } + } + } + + /** + * write the encoded result to stdout or a file if the option is specified + * + * @param rlpEncodedOutput the RLP output to write to file or stdout + */ + private void writeOutput(final BytesValue rlpEncodedOutput) { + if (rlpTargetFile != null) { + final Path targetPath = rlpTargetFile.toPath(); + + try (final BufferedWriter fileWriter = Files.newBufferedWriter(targetPath, UTF_8)) { + fileWriter.write(rlpEncodedOutput.toString()); + } catch (final IOException e) { + throw new ParameterException( + spec.commandLine(), + "An error occurred while trying to write the RLP string. " + e.getMessage()); + } + } else { + parentCommand.out.println(rlpEncodedOutput); + } + } + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/RLPType.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/RLPType.java new file mode 100644 index 0000000000..c2115278db --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/rlp/RLPType.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.cli.rlp; + +/** Type of the RLP data to encode/decode */ +public enum RLPType { + // Enum is used to enable the listing of the possible values in PicoCLI. + IBFT_EXTRA_DATA(new IbftExtraDataCLIAdapter()); + + private final JSONToRLP adapter; + + RLPType(final JSONToRLP adapter) { + + this.adapter = adapter; + } + + public JSONToRLP getAdapter() { + return adapter; + } +} diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java index 74eb045b0f..3a2492b098 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java @@ -30,6 +30,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.InputStream; import java.io.PrintStream; import java.net.URI; import java.nio.file.Path; @@ -58,10 +59,10 @@ public abstract class CommandTestAbstract { private final Logger TEST_LOGGER = LogManager.getLogger(); - final ByteArrayOutputStream commandOutput = new ByteArrayOutputStream(); + protected final ByteArrayOutputStream commandOutput = new ByteArrayOutputStream(); private final PrintStream outPrintStream = new PrintStream(commandOutput); - final ByteArrayOutputStream commandErrorOutput = new ByteArrayOutputStream(); + protected final ByteArrayOutputStream commandErrorOutput = new ByteArrayOutputStream(); private final PrintStream errPrintStream = new PrintStream(commandErrorOutput); @Mock RunnerBuilder mockRunnerBuilder; @@ -134,7 +135,11 @@ public void displayOutput() { TEST_LOGGER.info("Standard error {}", commandErrorOutput.toString()); } - CommandLine.Model.CommandSpec parseCommand(final String... args) { + protected CommandLine.Model.CommandSpec parseCommand(final String... args) { + return parseCommand(System.in, args); + } + + protected CommandLine.Model.CommandSpec parseCommand(final InputStream in, final String... args) { // turn off ansi usage globally in picocli System.setProperty("picocli.ansi", "false"); @@ -150,6 +155,7 @@ CommandLine.Model.CommandSpec parseCommand(final String... args) { pantheonCommand.parse( new RunLast().useOut(outPrintStream).useAnsi(Ansi.OFF), new DefaultExceptionHandler>().useErr(errPrintStream).useAnsi(Ansi.OFF), + in, args); return pantheonCommand.spec; } diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/rlp/RLPSubCommandTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/rlp/RLPSubCommandTest.java new file mode 100644 index 0000000000..93c1bae29c --- /dev/null +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/rlp/RLPSubCommandTest.java @@ -0,0 +1,211 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.cli.rlp; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; + +import tech.pegasys.pantheon.cli.CommandTestAbstract; + +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.nio.file.Files; + +import org.junit.After; +import org.junit.Test; +import picocli.CommandLine.Model.CommandSpec; + +public class RLPSubCommandTest extends CommandTestAbstract { + + private static final String EXPECTED_RLP_USAGE = + "Usage: pantheon rlp [-hV] [COMMAND]" + + System.lineSeparator() + + "This command provides RLP data related actions." + + System.lineSeparator() + + " -h, --help Show this help message and exit." + + System.lineSeparator() + + " -V, --version Print version information and exit." + + System.lineSeparator() + + "Commands:" + + System.lineSeparator() + + " encode This command encodes a JSON typed data into an RLP hex string."; + + private static final String EXPECTED_RLP_ENCODE_USAGE = + "Usage: pantheon rlp encode [-hV] [--from=] [--to=] [--type=]" + + System.lineSeparator() + + "This command encodes a JSON typed data into an RLP hex string." + + System.lineSeparator() + + " --from= File containing JSON object to encode" + + System.lineSeparator() + + " --to= File to write encoded RLP string to." + + System.lineSeparator() + + " --type= Type of the RLP data to encode, possible values are" + + System.lineSeparator() + + " IBFT_EXTRA_DATA. (default: IBFT_EXTRA_DATA)" + + System.lineSeparator() + + " -h, --help Show this help message and exit." + + System.lineSeparator() + + " -V, --version Print version information and exit."; + + private static final String RLP_SUBCOMMAND_NAME = "rlp"; + private static final String RLP_ENCODE_SUBCOMMAND_NAME = "encode"; + + // RLP sub-command + @Test + public void rlpSubCommandExistAnbHaveSubCommands() { + final CommandSpec spec = parseCommand(); + assertThat(spec.subcommands()).containsKeys(RLP_SUBCOMMAND_NAME); + assertThat(spec.subcommands().get(RLP_SUBCOMMAND_NAME).getSubcommands()) + .containsKeys(RLP_ENCODE_SUBCOMMAND_NAME); + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void callingRLPSubCommandWithoutSubSubcommandMustDisplayUsage() { + parseCommand(RLP_SUBCOMMAND_NAME); + assertThat(commandOutput.toString()).startsWith(EXPECTED_RLP_USAGE); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void callingRPLSubCommandHelpMustDisplayUsage() { + parseCommand(RLP_SUBCOMMAND_NAME, "--help"); + assertThat(commandOutput.toString()).startsWith(EXPECTED_RLP_USAGE); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + // Encode RLP sub-command + @Test + public void callingRPLEncodeSubCommandHelpMustDisplayUsage() { + parseCommand(RLP_SUBCOMMAND_NAME, RLP_ENCODE_SUBCOMMAND_NAME, "--help"); + assertThat(commandOutput.toString()).startsWith(EXPECTED_RLP_ENCODE_USAGE); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void encodeWithoutPathMustWriteToStandardOutput() { + + final String jsonInput = + "[\"be068f726a13c8d46c44be6ce9d275600e1735a4\", \"5ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193\"]"; + + // set stdin + final ByteArrayInputStream stdIn = new ByteArrayInputStream(jsonInput.getBytes(UTF_8)); + + parseCommand(stdIn, RLP_SUBCOMMAND_NAME, RLP_ENCODE_SUBCOMMAND_NAME); + + final String expectedRlpString = + "0xf853a00000000000000000000000000000000000000000000000000000000000000000ea94be068f726a13c8d" + + "46c44be6ce9d275600e1735a4945ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193808400000000c0"; + assertThat(commandOutput.toString()).contains(expectedRlpString); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void encodeWithOutputFileMustWriteInThisFile() throws Exception { + + final File file = File.createTempFile("ibftExtraData", "rlp"); + + final String jsonInput = + "[\"be068f726a13c8d46c44be6ce9d275600e1735a4\", \"5ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193\"]"; + + // set stdin + final ByteArrayInputStream stdIn = new ByteArrayInputStream(jsonInput.getBytes(UTF_8)); + + parseCommand(stdIn, RLP_SUBCOMMAND_NAME, RLP_ENCODE_SUBCOMMAND_NAME, "--to", file.getPath()); + + final String expectedRlpString = + "0xf853a00000000000000000000000000000000000000000000000000000000000000000ea94be068f726a13c8d" + + "46c44be6ce9d275600e1735a4945ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193808400000000c0"; + + assertThat(contentOf(file)).contains(expectedRlpString); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void encodeWithInputFilePathMustReadFromThisFile() throws Exception { + + final File tempJsonFile = temp.newFile("test.json"); + try (final BufferedWriter fileWriter = Files.newBufferedWriter(tempJsonFile.toPath(), UTF_8)) { + + fileWriter.write( + "[\"be068f726a13c8d46c44be6ce9d275600e1735a4\", \"5ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193\"]"); + + fileWriter.flush(); + + parseCommand( + RLP_SUBCOMMAND_NAME, RLP_ENCODE_SUBCOMMAND_NAME, "--from", tempJsonFile.getPath()); + + final String expectedRlpString = + "0xf853a00000000000000000000000000000000000000000000000000000000000000000ea94be068f726a13c8d" + + "46c44be6ce9d275600e1735a4945ff6f4b66a46a2b2310a6f3a93aaddc0d9a1c193808400000000c0"; + assertThat(commandOutput.toString()).contains(expectedRlpString); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + } + + @Test + public void encodeWithInvalidInputMustRaiseAnError() throws Exception { + + final File tempJsonFile = temp.newFile("invalid_test.json"); + try (final BufferedWriter fileWriter = Files.newBufferedWriter(tempJsonFile.toPath(), UTF_8)) { + + fileWriter.write("{\"property\":0}"); + + fileWriter.flush(); + + parseCommand( + RLP_SUBCOMMAND_NAME, RLP_ENCODE_SUBCOMMAND_NAME, "--from", tempJsonFile.getPath()); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()) + .startsWith( + "Unable to map the JSON data with selected type. Please check JSON input format."); + } + } + + @Test + public void encodeWithEmptyInputMustRaiseAnError() throws Exception { + + final File tempJsonFile = temp.newFile("empty.json"); + + parseCommand(RLP_SUBCOMMAND_NAME, RLP_ENCODE_SUBCOMMAND_NAME, "--from", tempJsonFile.getPath()); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()) + .startsWith("An error occurred while trying to read the JSON data."); + } + + @Test + public void encodeWithEmptyStdInputMustRaiseAnError() throws Exception { + + // set empty stdin + final String jsonInput = ""; + final ByteArrayInputStream stdIn = new ByteArrayInputStream(jsonInput.getBytes(UTF_8)); + + parseCommand(stdIn, RLP_SUBCOMMAND_NAME, RLP_ENCODE_SUBCOMMAND_NAME); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()) + .startsWith("An error occurred while trying to read the JSON data."); + } + + @After + public void restoreStdin() { + System.setIn(System.in); + } +}