Fix Array Configurable CLI options (#514)

* Fix Array Configurable CLI options

--host-whitelist and --rpc-cors-origins could not be configured as
a TOML array.  The underlying PicoCLI issues were resolved with
revamped property types that act like Collections.

A "configure everything" test is added that creates a TOML file that
requires all CLI options to be configurable and configured in
a new everything_config.toml test file.
Danno Ferrin 6 years ago committed by GitHub
parent dcedfeabb1
commit 8d4048b988
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java
  2. 78
      pantheon/src/main/java/tech/pegasys/pantheon/cli/custom/CorsAllowedOriginsProperty.java
  3. 75
      pantheon/src/main/java/tech/pegasys/pantheon/cli/custom/JsonRPCWhitelistHostsProperty.java
  4. 21
      pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java
  5. 228
      pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java
  6. 63
      pantheon/src/test/resources/everything_config.toml

@ -277,8 +277,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
// A list of origins URLs that are accepted by the JsonRpcHttpServer (CORS) // A list of origins URLs that are accepted by the JsonRpcHttpServer (CORS)
@Option( @Option(
names = {"--rpc-cors-origins"}, names = {"--rpc-cors-origins"},
description = "Comma separated origin domain URLs for CORS validation (default: none)", description = "Comma separated origin domain URLs for CORS validation (default: none)"
converter = CorsAllowedOriginsProperty.CorsAllowedOriginsPropertyConverter.class
) )
private final CorsAllowedOriginsProperty rpcCorsAllowedOrigins = new CorsAllowedOriginsProperty(); private final CorsAllowedOriginsProperty rpcCorsAllowedOrigins = new CorsAllowedOriginsProperty();
@ -331,7 +330,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
+ "default: ${DEFAULT-VALUE}", + "default: ${DEFAULT-VALUE}",
defaultValue = "" + WebSocketConfiguration.DEFAULT_WEBSOCKET_REFRESH_DELAY defaultValue = "" + WebSocketConfiguration.DEFAULT_WEBSOCKET_REFRESH_DELAY
) )
private void setRefreshDelay(final Long refreshDelay) { private Long configureRefreshDelay(final Long refreshDelay) {
if (refreshDelay < DEFAULT_MIN_REFRESH_DELAY || refreshDelay > DEFAULT_MAX_REFRESH_DELAY) { if (refreshDelay < DEFAULT_MIN_REFRESH_DELAY || refreshDelay > DEFAULT_MAX_REFRESH_DELAY) {
throw new ParameterException( throw new ParameterException(
new CommandLine(this), new CommandLine(this),
@ -341,6 +340,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
String.valueOf(DEFAULT_MAX_REFRESH_DELAY))); String.valueOf(DEFAULT_MAX_REFRESH_DELAY)));
} }
this.refreshDelay = refreshDelay; this.refreshDelay = refreshDelay;
return refreshDelay;
} }
@Option( @Option(
@ -363,8 +363,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
paramLabel = "<hostname>", paramLabel = "<hostname>",
description = description =
"Comma separated list of hostnames to whitelist for RPC access. default: ${DEFAULT-VALUE}", "Comma separated list of hostnames to whitelist for RPC access. default: ${DEFAULT-VALUE}",
defaultValue = "localhost", defaultValue = "localhost"
converter = JsonRPCWhitelistHostsProperty.JsonRPCWhitelistHostsConverter.class
) )
private final JsonRPCWhitelistHostsProperty hostsWhitelist = new JsonRPCWhitelistHostsProperty(); private final JsonRPCWhitelistHostsProperty hostsWhitelist = new JsonRPCWhitelistHostsProperty();
@ -579,9 +578,9 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
jsonRpcConfiguration.setEnabled(isJsonRpcEnabled); jsonRpcConfiguration.setEnabled(isJsonRpcEnabled);
jsonRpcConfiguration.setHost(rpcHostAndPort.getHost()); jsonRpcConfiguration.setHost(rpcHostAndPort.getHost());
jsonRpcConfiguration.setPort(rpcHostAndPort.getPort()); jsonRpcConfiguration.setPort(rpcHostAndPort.getPort());
jsonRpcConfiguration.setCorsAllowedDomains(rpcCorsAllowedOrigins.getDomains()); jsonRpcConfiguration.setCorsAllowedDomains(rpcCorsAllowedOrigins);
jsonRpcConfiguration.setRpcApis(rpcApis); jsonRpcConfiguration.setRpcApis(rpcApis);
jsonRpcConfiguration.setHostsWhitelist(hostsWhitelist.hostnamesWhitelist()); jsonRpcConfiguration.setHostsWhitelist(hostsWhitelist);
return jsonRpcConfiguration; return jsonRpcConfiguration;
} }
@ -600,7 +599,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
metricsConfiguration.setEnabled(isMetricsEnabled); metricsConfiguration.setEnabled(isMetricsEnabled);
metricsConfiguration.setHost(metricsHostAndPort.getHost()); metricsConfiguration.setHost(metricsHostAndPort.getHost());
metricsConfiguration.setPort(metricsHostAndPort.getPort()); metricsConfiguration.setPort(metricsHostAndPort.getPort());
metricsConfiguration.setHostsWhitelist(hostsWhitelist.hostnamesWhitelist()); metricsConfiguration.setHostsWhitelist(hostsWhitelist);
return metricsConfiguration; return metricsConfiguration;
} }

@ -12,72 +12,80 @@
*/ */
package tech.pegasys.pantheon.cli.custom; package tech.pegasys.pantheon.cli.custom;
import java.util.AbstractCollection;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.StringJoiner; import java.util.StringJoiner;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException; import java.util.regex.PatternSyntaxException;
import javax.annotation.Nonnull;
import com.google.common.collect.Lists; import com.google.common.base.Splitter;
import picocli.CommandLine.ITypeConverter; import com.google.common.base.Strings;
public class CorsAllowedOriginsProperty { public class CorsAllowedOriginsProperty extends AbstractCollection<String> {
private List<String> domains = Collections.emptyList(); private final List<String> domains = new ArrayList<>();
public CorsAllowedOriginsProperty(final List<String> domains) {
this.domains = domains;
}
public CorsAllowedOriginsProperty() {} public CorsAllowedOriginsProperty() {}
public List<String> getDomains() { @Override
return domains; @Nonnull
public Iterator<String> iterator() {
if (domains.size() == 1 && domains.get(0).equals("none")) {
return Collections.emptyIterator();
} else {
return domains.iterator();
}
} }
public static class CorsAllowedOriginsPropertyConverter @Override
implements ITypeConverter<CorsAllowedOriginsProperty> { public int size() {
return domains.size();
}
@Override @Override
public CorsAllowedOriginsProperty convert(final String value) throws IllegalArgumentException { public boolean add(final String string) {
final List<String> domains; return addAll(Collections.singleton(string));
if (value != null && !value.isEmpty()) {
domains = new ArrayList<>(Arrays.asList(value.split("\\s*,\\s*")));
} else {
throw new IllegalArgumentException("Property can't be null/empty string");
} }
if (domains.contains("none")) { @Override
if (domains.size() > 1) { public boolean addAll(final Collection<? extends String> collection) {
throw new IllegalArgumentException("Value 'none' can't be used with other domains"); final int initialSize = domains.size();
for (final String string : collection) {
if (Strings.isNullOrEmpty(string)) {
throw new IllegalArgumentException("Domain cannot be empty string or null string.");
}
for (final String s : Splitter.onPattern("\\s*,+\\s*").split(string)) {
if ("all".equals(s)) {
domains.add("*");
} else { } else {
return new CorsAllowedOriginsProperty(Collections.emptyList()); domains.add(s);
}
} }
} }
if (domains.contains("all") || domains.contains("*")) { if (domains.contains("none")) {
if (domains.size() > 1) { if (domains.size() > 1) {
throw new IllegalArgumentException("Value 'all' can't be used with other domains"); throw new IllegalArgumentException("Value 'none' can't be used with other domains");
} else {
return new CorsAllowedOriginsProperty(Lists.newArrayList("*"));
} }
} else if (domains.contains("*")) {
if (domains.size() > 1) {
throw new IllegalArgumentException("Values '*' or 'all' can't be used with other domains");
} }
} else {
try { try {
final StringJoiner stringJoiner = new StringJoiner("|"); final StringJoiner stringJoiner = new StringJoiner("|");
domains.stream().filter(s -> !s.isEmpty()).forEach(stringJoiner::add); domains.forEach(stringJoiner::add);
Pattern.compile(stringJoiner.toString()); Pattern.compile(stringJoiner.toString());
} catch (final PatternSyntaxException e) { } catch (final PatternSyntaxException e) {
throw new IllegalArgumentException("Domain values result in invalid regex pattern", e); throw new IllegalArgumentException("Domain values result in invalid regex pattern", e);
} }
if (domains.size() > 0) {
return new CorsAllowedOriginsProperty(domains);
} else {
return new CorsAllowedOriginsProperty();
}
} }
return domains.size() != initialSize;
} }
} }

@ -12,59 +12,70 @@
*/ */
package tech.pegasys.pantheon.cli.custom; package tech.pegasys.pantheon.cli.custom;
import java.util.AbstractCollection;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator;
import java.util.List; import java.util.List;
import javax.annotation.Nonnull;
import picocli.CommandLine; import com.google.common.base.Splitter;
import com.google.common.base.Strings;
public class JsonRPCWhitelistHostsProperty { public class JsonRPCWhitelistHostsProperty extends AbstractCollection<String> {
private List<String> hostnamesWhitelist = Collections.emptyList();
private JsonRPCWhitelistHostsProperty(final List<String> hostnamesWhitelist) { private final List<String> hostnamesWhitelist = new ArrayList<>();
this.hostnamesWhitelist = hostnamesWhitelist;
}
public JsonRPCWhitelistHostsProperty() {} public JsonRPCWhitelistHostsProperty() {}
public List<String> hostnamesWhitelist() {
return hostnamesWhitelist;
}
public static class JsonRPCWhitelistHostsConverter
implements CommandLine.ITypeConverter<JsonRPCWhitelistHostsProperty> {
@Override @Override
public JsonRPCWhitelistHostsProperty convert(final String value) { @Nonnull
final List<String> hostNames; public Iterator<String> iterator() {
if (value != null && !value.isEmpty()) { if (hostnamesWhitelist.size() == 1 && hostnamesWhitelist.get(0).equals("none")) {
hostNames = new ArrayList<>(Arrays.asList(value.split("\\s*,\\s*"))); return Collections.emptyIterator();
} else { } else {
throw new IllegalArgumentException("Property can't be null/empty string"); return hostnamesWhitelist.iterator();
}
} }
if (hostNames.contains("all")) { @Override
if (hostNames.size() > 1) { public int size() {
throw new IllegalArgumentException("Value 'all' can't be used with other hostnames"); return hostnamesWhitelist.size();
} else {
return new JsonRPCWhitelistHostsProperty(Collections.singletonList("*"));
} }
@Override
public boolean add(final String string) {
return addAll(Collections.singleton(string));
} }
if (hostNames.contains("*")) { @Override
if (hostNames.size() > 1) { public boolean addAll(final Collection<? extends String> collection) {
throw new IllegalArgumentException("Value '*' can't be used with other hostnames"); final int initialSize = hostnamesWhitelist.size();
for (final String string : collection) {
if (Strings.isNullOrEmpty(string)) {
throw new IllegalArgumentException("Hostname cannot be empty string or null string.");
}
for (final String s : Splitter.onPattern("\\s*,+\\s*").split(string)) {
if ("all".equals(s)) {
hostnamesWhitelist.add("*");
} else { } else {
return new JsonRPCWhitelistHostsProperty(Collections.singletonList("*")); hostnamesWhitelist.add(s);
}
} }
} }
if (hostNames.size() > 0) { if (hostnamesWhitelist.contains("none")) {
return new JsonRPCWhitelistHostsProperty(hostNames); if (hostnamesWhitelist.size() > 1) {
} else { throw new IllegalArgumentException("Value 'none' can't be used with other hostnames");
return new JsonRPCWhitelistHostsProperty(); }
} else if (hostnamesWhitelist.contains("*")) {
if (hostnamesWhitelist.size() > 1) {
throw new IllegalArgumentException(
"Values '*' or 'all' can't be used with other hostnames");
} }
} }
return hostnamesWhitelist.size() != initialSize;
} }
} }

@ -44,6 +44,7 @@ import org.mockito.Captor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import picocli.CommandLine;
import picocli.CommandLine.DefaultExceptionHandler; import picocli.CommandLine.DefaultExceptionHandler;
import picocli.CommandLine.Help.Ansi; import picocli.CommandLine.Help.Ansi;
import picocli.CommandLine.RunLast; import picocli.CommandLine.RunLast;
@ -101,10 +102,10 @@ public abstract class CommandTestAbstract {
LOG.info("Standard error {}", commandErrorOutput.toString()); LOG.info("Standard error {}", commandErrorOutput.toString());
} }
void parseCommand(final String... args) { CommandLine.Model.CommandSpec parseCommand(final String... args) {
final PantheonCommand pantheonCommand = final TestPantheonCommand pantheonCommand =
new PantheonCommand( new TestPantheonCommand(
mockBlockImporter, mockRunnerBuilder, mockControllerBuilder, mockSyncConfBuilder); mockBlockImporter, mockRunnerBuilder, mockControllerBuilder, mockSyncConfBuilder);
// parse using Ansi.OFF to be able to assert on non formatted output results // parse using Ansi.OFF to be able to assert on non formatted output results
@ -112,5 +113,19 @@ public abstract class CommandTestAbstract {
new RunLast().useOut(outPrintStream).useAnsi(Ansi.OFF), new RunLast().useOut(outPrintStream).useAnsi(Ansi.OFF),
new DefaultExceptionHandler<List<Object>>().useErr(errPrintStream).useAnsi(Ansi.OFF), new DefaultExceptionHandler<List<Object>>().useErr(errPrintStream).useAnsi(Ansi.OFF),
args); args);
return pantheonCommand.spec;
}
@CommandLine.Command
static class TestPantheonCommand extends PantheonCommand {
@CommandLine.Spec CommandLine.Model.CommandSpec spec;
TestPantheonCommand(
final BlockImporter mockBlockImporter,
final RunnerBuilder mockRunnerBuilder,
final PantheonControllerBuilder mockControllerBuilder,
final SynchronizerConfiguration.Builder mockSyncConfBuilder) {
super(mockBlockImporter, mockRunnerBuilder, mockControllerBuilder, mockSyncConfBuilder);
}
} }
} }

@ -50,13 +50,15 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.google.common.collect.Lists;
import com.google.common.io.Resources; import com.google.common.io.Resources;
import net.consensys.cava.toml.Toml;
import net.consensys.cava.toml.TomlParseResult;
import org.apache.commons.text.StringEscapeUtils; import org.apache.commons.text.StringEscapeUtils;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
@ -65,6 +67,7 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers; import org.mockito.ArgumentMatchers;
import picocli.CommandLine;
public class PantheonCommandTest extends CommandTestAbstract { public class PantheonCommandTest extends CommandTestAbstract {
private final String VALID_NODE_ID = private final String VALID_NODE_ID =
@ -76,13 +79,13 @@ public class PantheonCommandTest extends CommandTestAbstract {
private static final MetricsConfiguration defaultMetricsConfiguration; private static final MetricsConfiguration defaultMetricsConfiguration;
private static final String GENESIS_CONFIG_TESTDATA = "genesis_config"; private static final String GENESIS_CONFIG_TESTDATA = "genesis_config";
final String[] validENodeStrings = { private final String[] validENodeStrings = {
"enode://" + VALID_NODE_ID + "@192.168.0.1:4567", "enode://" + VALID_NODE_ID + "@192.168.0.1:4567",
"enode://" + VALID_NODE_ID + "@192.168.0.2:4567", "enode://" + VALID_NODE_ID + "@192.168.0.2:4567",
"enode://" + VALID_NODE_ID + "@192.168.0.3:4567" "enode://" + VALID_NODE_ID + "@192.168.0.3:4567"
}; };
final String[] ropstenBootnodes = { private final String[] ropstenBootnodes = {
"enode://6332792c4a00e3e4ee0926ed89e0d27ef985424d97b6a45bf0f23e51f0dcb5e66b875777506458aea7af6f9e4ffb69f43f3778ee73c81ed9d34c51c4b16b0b0f@52.232.243.152:30303", "enode://6332792c4a00e3e4ee0926ed89e0d27ef985424d97b6a45bf0f23e51f0dcb5e66b875777506458aea7af6f9e4ffb69f43f3778ee73c81ed9d34c51c4b16b0b0f@52.232.243.152:30303",
"enode://94c15d1b9e2fe7ce56e458b9a3b672ef11894ddedd0c6f247e0f1d3487f52b66208fb4aeb8179fce6e3a749ea93ed147c37976d67af557508d199d9594c35f09@192.81.208.223:30303" "enode://94c15d1b9e2fe7ce56e458b9a3b672ef11894ddedd0c6f247e0f1d3487f52b66208fb4aeb8179fce6e3a749ea93ed147c37976d67af557508d199d9594c35f09@192.81.208.223:30303"
}; };
@ -98,8 +101,7 @@ public class PantheonCommandTest extends CommandTestAbstract {
websocketConf.addRpcApi(IbftRpcApis.IBFT); websocketConf.addRpcApi(IbftRpcApis.IBFT);
defaultWebSocketConfiguration = websocketConf; defaultWebSocketConfiguration = websocketConf;
final MetricsConfiguration metricsConf = MetricsConfiguration.createDefault(); defaultMetricsConfiguration = MetricsConfiguration.createDefault();
defaultMetricsConfiguration = metricsConf;
} }
@Before @Before
@ -339,6 +341,47 @@ public class PantheonCommandTest extends CommandTestAbstract {
assertThat(commandErrorOutput.toString()).isEmpty(); assertThat(commandErrorOutput.toString()).isEmpty();
} }
@Test
public void tomlThatConfiguresEverything() throws IOException {
assumeTrue(isFullInstantiation());
// Load a TOML that configures literally everything
final URL configFile = Resources.getResource("everything_config.toml");
final Path toml = Files.createTempFile("toml", "");
Files.write(toml, Resources.toByteArray(configFile));
// Parse it.
final CommandLine.Model.CommandSpec spec = parseCommand("--config", toml.toString());
final TomlParseResult tomlResult = Toml.parse(toml);
// Verify we configured everything
final HashSet<CommandLine.Model.OptionSpec> options = new HashSet<>(spec.options());
// Except for meta-options
options.remove(spec.optionsMap().get("--config"));
options.remove(spec.optionsMap().get("--help"));
options.remove(spec.optionsMap().get("--version"));
for (final String tomlKey : tomlResult.keySet()) {
final CommandLine.Model.OptionSpec optionSpec = spec.optionsMap().get("--" + tomlKey);
assertThat(optionSpec)
.describedAs("Option '%s' should be a configurable option.", tomlKey)
.isNotNull();
// Verify TOML stores it by the appropriate type
if (optionSpec.type().equals(Boolean.class)) {
tomlResult.getBoolean(tomlKey);
} else if (optionSpec.isMultiValue() || optionSpec.arity().max > 1) {
tomlResult.getArray(tomlKey);
} else if (Number.class.isAssignableFrom(optionSpec.type())) {
tomlResult.getLong(tomlKey);
} else {
tomlResult.getString(tomlKey);
}
options.remove(optionSpec);
}
assertThat(options.stream().map(CommandLine.Model.OptionSpec::longestName)).isEmpty();
}
@Test @Test
public void noOverrideDefaultValuesIfKeyIsNotPresentInConfigFile() throws IOException { public void noOverrideDefaultValuesIfKeyIsNotPresentInConfigFile() throws IOException {
assumeTrue(isFullInstantiation()); assumeTrue(isFullInstantiation());
@ -823,6 +866,21 @@ public class PantheonCommandTest extends CommandTestAbstract {
assertThat(commandErrorOutput.toString()).isEmpty(); assertThat(commandErrorOutput.toString()).isEmpty();
} }
@Test
public void jsonRpcCorsOriginsDoubleCommaFilteredOut() {
final String[] origins = {"http://domain1.com", "https://domain2.com"};
parseCommand("--rpc-cors-origins", String.join(",,", origins));
verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains().toArray())
.isEqualTo(origins);
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
@Test @Test
public void jsonRpcCorsOriginsWithWildcardMustBuildListWithWildcard() { public void jsonRpcCorsOriginsWithWildcardMustBuildListWithWildcard() {
final String[] origins = {"*"}; final String[] origins = {"*"};
@ -840,14 +898,12 @@ public class PantheonCommandTest extends CommandTestAbstract {
@Test @Test
public void jsonRpcCorsOriginsWithAllMustBuildListWithWildcard() { public void jsonRpcCorsOriginsWithAllMustBuildListWithWildcard() {
final String[] origins = {"all"}; parseCommand("--rpc-cors-origins", "all");
parseCommand("--rpc-cors-origins", String.join(",", origins));
verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build(); verify(mockRunnerBuilder).build();
assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains()) assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains()).containsExactly("*");
.isEqualTo(Lists.newArrayList("*"));
assertThat(commandOutput.toString()).isEmpty(); assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty(); assertThat(commandErrorOutput.toString()).isEmpty();
@ -880,27 +936,59 @@ public class PantheonCommandTest extends CommandTestAbstract {
} }
@Test @Test
public void jsonRpcCorsOriginsAllWithAnotherDomainMustFail() { public void jsonRpcCorsOriginsNoneWithAnotherDomainMustFailNoneFirst() {
final String[] origins = {"http://domain1.com", "all"}; final String[] origins = {"none", "http://domain1.com"};
parseCommand("--rpc-cors-origins", String.join(",", origins)); parseCommand("--rpc-cors-origins", String.join(",", origins));
verifyZeroInteractions(mockRunnerBuilder); verifyZeroInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString()).isEmpty(); assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()) assertThat(commandErrorOutput.toString())
.contains("Value 'all' can't be used with other domains"); .contains("Value 'none' can't be used with other domains");
}
@Test
public void jsonRpcCorsOriginsAllWithAnotherDomainMustFail() {
parseCommand("--rpc-cors-origins=http://domain1.com,all");
verifyZeroInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains("Values '*' or 'all' can't be used with other domains");
}
@Test
public void jsonRpcCorsOriginsAllWithAnotherDomainMustFailAsFlags() {
parseCommand("--rpc-cors-origins=http://domain1.com", "--rpc-cors-origins=all");
verifyZeroInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains("Values '*' or 'all' can't be used with other domains");
} }
@Test @Test
public void jsonRpcCorsOriginsWildcardWithAnotherDomainMustFail() { public void jsonRpcCorsOriginsWildcardWithAnotherDomainMustFail() {
final String[] origins = {"http://domain1.com", "*"}; parseCommand("--rpc-cors-origins=http://domain1.com,*");
parseCommand("--rpc-cors-origins", String.join(",", origins));
verifyZeroInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains("Values '*' or 'all' can't be used with other domains");
}
@Test
public void jsonRpcCorsOriginsWildcardWithAnotherDomainMustFailAsFlags() {
parseCommand("--rpc-cors-origins=http://domain1.com", "--rpc-cors-origins=*");
verifyZeroInteractions(mockRunnerBuilder); verifyZeroInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString()).isEmpty(); assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()) assertThat(commandErrorOutput.toString())
.contains("Value 'all' can't be used with other domains"); .contains("Values '*' or 'all' can't be used with other domains");
} }
@Test @Test
@ -915,6 +1003,17 @@ public class PantheonCommandTest extends CommandTestAbstract {
.contains("Domain values result in invalid regex pattern"); .contains("Domain values result in invalid regex pattern");
} }
@Test
public void jsonRpcCorsOriginsEmtyValueFails() {
parseCommand("--rpc-cors-origins=");
verifyZeroInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains("Domain cannot be empty string or null string.");
}
@Test @Test
public void jsonRpcHostWhitelistAcceptsSingleArgument() { public void jsonRpcHostWhitelistAcceptsSingleArgument() {
parseCommand("--host-whitelist", "a"); parseCommand("--host-whitelist", "a");
@ -947,6 +1046,38 @@ public class PantheonCommandTest extends CommandTestAbstract {
assertThat(commandErrorOutput.toString()).isEmpty(); assertThat(commandErrorOutput.toString()).isEmpty();
} }
@Test
public void jsonRpcHostWhitelistAcceptsDoubleComma() {
parseCommand("--host-whitelist", "a,,b");
verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(jsonRpcConfigArgumentCaptor.getValue().getHostsWhitelist().size()).isEqualTo(2);
assertThat(jsonRpcConfigArgumentCaptor.getValue().getHostsWhitelist()).contains("a", "b");
assertThat(jsonRpcConfigArgumentCaptor.getValue().getHostsWhitelist())
.doesNotContain("*", "localhost");
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
@Test
public void jsonRpcHostWhitelistAcceptsMultipleFlags() {
parseCommand("--host-whitelist=a", "--host-whitelist=b");
verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(jsonRpcConfigArgumentCaptor.getValue().getHostsWhitelist().size()).isEqualTo(2);
assertThat(jsonRpcConfigArgumentCaptor.getValue().getHostsWhitelist()).contains("a", "b");
assertThat(jsonRpcConfigArgumentCaptor.getValue().getHostsWhitelist())
.doesNotContain("*", "localhost");
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
@Test @Test
public void jsonRpcHostWhitelistStarWithAnotherHostnameMustFail() { public void jsonRpcHostWhitelistStarWithAnotherHostnameMustFail() {
final String[] origins = {"friend", "*"}; final String[] origins = {"friend", "*"};
@ -956,7 +1087,19 @@ public class PantheonCommandTest extends CommandTestAbstract {
assertThat(commandOutput.toString()).isEmpty(); assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()) assertThat(commandErrorOutput.toString())
.contains("Value '*' can't be used with other hostnames"); .contains("Values '*' or 'all' can't be used with other hostnames");
}
@Test
public void jsonRpcHostWhitelistStarWithAnotherHostnameMustFailStarFirst() {
final String[] origins = {"*", "friend"};
parseCommand("--host-whitelist", String.join(",", origins));
verifyZeroInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains("Values '*' or 'all' can't be used with other hostnames");
} }
@Test @Test
@ -968,7 +1111,56 @@ public class PantheonCommandTest extends CommandTestAbstract {
assertThat(commandOutput.toString()).isEmpty(); assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()) assertThat(commandErrorOutput.toString())
.contains("Value 'all' can't be used with other hostnames"); .contains("Values '*' or 'all' can't be used with other hostnames");
}
@Test
public void jsonRpcHostWhitelistWithNoneMustBuildEmptyList() {
final String[] origins = {"none"};
parseCommand("--host-whitelist", String.join(",", origins));
verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(jsonRpcConfigArgumentCaptor.getValue().getHostsWhitelist()).isEmpty();
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
@Test
public void jsonRpcHostWhitelistNoneWithAnotherDomainMustFail() {
final String[] origins = {"http://domain1.com", "none"};
parseCommand("--host-whitelist", String.join(",", origins));
verifyZeroInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains("Value 'none' can't be used with other hostnames");
}
@Test
public void jsonRpcHostWhitelistNoneWithAnotherDomainMustFailNoneFirst() {
final String[] origins = {"none", "http://domain1.com"};
parseCommand("--host-whitelist", String.join(",", origins));
verifyZeroInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains("Value 'none' can't be used with other hostnames");
}
@Test
public void jsonRpcHostWhitelistEmptyValueFails() {
parseCommand("--host-whitelist=");
verifyZeroInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains("Hostname cannot be empty string or null string.");
} }
@Test @Test
@ -1175,7 +1367,7 @@ public class PantheonCommandTest extends CommandTestAbstract {
} }
@Test @Test
public void noSeveralNetworkOptions() throws Exception { public void noSeveralNetworkOptions() {
parseCommand("--goerli", "--rinkeby"); parseCommand("--goerli", "--rinkeby");
verifyZeroInteractions(mockRunnerBuilder); verifyZeroInteractions(mockRunnerBuilder);

@ -0,0 +1,63 @@
# Every possible CLI should be in this file.
# The odds are you are reading this because you added a CLI and didn't add it
# here and a test broke. To fix the test add your CLI to this file.
#
# Please use a plausable value, Pantheon has to at least be able to parse it.
# If it is a multi-valued CLI make it a TOML array.
# If it is a number or boolean make it a number or boolean
# All other config options are strings, and must be quoted.
# Please provide some sensible grouping.
# Node Information
datadir="~/pantheondata"
logging="INFO"
node-private-key="0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
# P2P network
no-discovery=true
bootnodes=[
"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.1:4567",
"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.1:4567",
"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.1:4567"
]
banned-nodeids=["0x6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0"]
p2p-listen="1.2.3.4:1234"
max-peers=42
max-trailing-peers=5
host-whitelist=["all"]
# chain
genesis="~/genesis.json"
#sync-mode="fast"
ottoman=false
ropsten=false
goerli=false
network-id=303
rinkeby=false
dev-mode=false
# JSON-RPC
rpc-enabled=false
rpc-listen="5.6.7.8:5678"
rpc-api=["DEBUG","ETH"]
rpc-cors-origins=["none"]
# WebSockets API
ws-enabled=false
ws-api=["DEBUG","ETH"]
ws-listen="9.10.11.12:9101"
ws-refresh-delay=500
# Prometheus Metrics Endpoint
metrics-enabled=false
metrics-listen="8.6.7.5:309"
# Mining
miner-enabled=false
miner-coinbase="0x0000000000000000000000000000000000000002"
miner-extraData="Protocol Engineering Group And SYStems"
miner-minTransactionGasPriceWei="1"
# Permissioning
accounts-whitelist=["0x0000000000000000000000000000000000000009"]
nodes-whitelist=["all"]
Loading…
Cancel
Save