fix: allow the cli to accept negative values (#33084)
also adding a unit testable picocli closes: #33068 Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
parent
b717810061
commit
d981f7f55d
11 changed files with 299 additions and 217 deletions
|
@ -615,6 +615,12 @@
|
|||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- twitter api -->
|
||||
<dependency>
|
||||
|
|
|
@ -22,7 +22,6 @@ import static org.keycloak.quarkus.runtime.Environment.isDevProfile;
|
|||
import static org.keycloak.quarkus.runtime.Environment.getProfileOrDefault;
|
||||
import static org.keycloak.quarkus.runtime.Environment.isNonServerMode;
|
||||
import static org.keycloak.quarkus.runtime.Environment.isTestLaunchMode;
|
||||
import static org.keycloak.quarkus.runtime.cli.Picocli.parseAndRun;
|
||||
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG;
|
||||
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.wasBuildEverRun;
|
||||
import static org.keycloak.quarkus.runtime.cli.command.Start.isDevProfileNotAllowed;
|
||||
|
@ -104,7 +103,7 @@ public class KeycloakMain implements QuarkusApplication {
|
|||
}
|
||||
|
||||
// parse arguments and execute any of the configured commands
|
||||
parseAndRun(cliArgs);
|
||||
new Picocli().parseAndRun(cliArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -94,7 +94,7 @@ import picocli.CommandLine.Model.ISetter;
|
|||
import picocli.CommandLine.Model.OptionSpec;
|
||||
import picocli.CommandLine.Model.ArgGroupSpec;
|
||||
|
||||
public final class Picocli {
|
||||
public class Picocli {
|
||||
|
||||
public static final String ARG_PREFIX = "--";
|
||||
public static final String ARG_SHORT_PREFIX = "-";
|
||||
|
@ -105,10 +105,7 @@ public final class Picocli {
|
|||
boolean includeBuildTime;
|
||||
}
|
||||
|
||||
private Picocli() {
|
||||
}
|
||||
|
||||
public static void parseAndRun(List<String> cliArgs) {
|
||||
public void parseAndRun(List<String> cliArgs) {
|
||||
// perform two passes over the cli args. First without option validation to determine the current command, then with option validation enabled
|
||||
CommandLine cmd = createCommandLine(spec -> spec
|
||||
.addUnmatchedArgsBinding(CommandLine.Model.UnmatchedArgsBinding.forStringArrayConsumer(new ISetter() {
|
||||
|
@ -135,7 +132,7 @@ public final class Picocli {
|
|||
exitCode = runReAugmentationIfNeeded(cliArgs, cmd, currentCommand);
|
||||
} else {
|
||||
PropertyMappers.sanitizeDisabledMappers();
|
||||
exitCode = cmd.execute(argArray);
|
||||
exitCode = run(cmd, argArray);
|
||||
}
|
||||
|
||||
exitOnFailure(exitCode, cmd);
|
||||
|
@ -146,7 +143,11 @@ public final class Picocli {
|
|||
}
|
||||
}
|
||||
|
||||
private static CommandLine createCommandLineForCommand(List<String> cliArgs, List<CommandLine> commandLineList) {
|
||||
protected int run(CommandLine cmd, String[] argArray) {
|
||||
return cmd.execute(argArray);
|
||||
}
|
||||
|
||||
private CommandLine createCommandLineForCommand(List<String> cliArgs, List<CommandLine> commandLineList) {
|
||||
return createCommandLine(spec -> {
|
||||
// use the incoming commandLineList from the initial parsing to determine the current command
|
||||
CommandSpec currentSpec = spec;
|
||||
|
@ -177,7 +178,7 @@ public final class Picocli {
|
|||
});
|
||||
}
|
||||
|
||||
private static void catchParameterException(ParameterException parEx, CommandLine cmd, String[] args) {
|
||||
private void catchParameterException(ParameterException parEx, CommandLine cmd, String[] args) {
|
||||
int exitCode;
|
||||
try {
|
||||
exitCode = cmd.getParameterExceptionHandler().handleParseException(parEx, args);
|
||||
|
@ -189,20 +190,20 @@ public final class Picocli {
|
|||
exitOnFailure(exitCode, cmd);
|
||||
}
|
||||
|
||||
private static void catchProfileException(String message, Throwable cause, CommandLine cmd) {
|
||||
private void catchProfileException(String message, Throwable cause, CommandLine cmd) {
|
||||
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
|
||||
errorHandler.error(cmd.getErr(), message, cause);
|
||||
exitOnFailure(CommandLine.ExitCode.USAGE, cmd);
|
||||
}
|
||||
|
||||
private static void exitOnFailure(int exitCode, CommandLine cmd) {
|
||||
protected void exitOnFailure(int exitCode, CommandLine cmd) {
|
||||
if (exitCode != cmd.getCommandSpec().exitCodeOnSuccess() && !Environment.isTestLaunchMode() || isRebuildCheck()) {
|
||||
// hard exit wanted, as build failed and no subsequent command should be executed. no quarkus involved.
|
||||
System.exit(exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
private static int runReAugmentationIfNeeded(List<String> cliArgs, CommandLine cmd, CommandLine currentCommand) {
|
||||
protected int runReAugmentationIfNeeded(List<String> cliArgs, CommandLine cmd, CommandLine currentCommand) {
|
||||
int exitCode = 0;
|
||||
|
||||
if (currentCommand == null) {
|
||||
|
@ -669,7 +670,7 @@ public final class Picocli {
|
|||
return key.startsWith("kc.provider.file");
|
||||
}
|
||||
|
||||
public static CommandLine createCommandLine(Consumer<CommandSpec> consumer) {
|
||||
public CommandLine createCommandLine(Consumer<CommandSpec> consumer) {
|
||||
CommandSpec spec = CommandSpec.forAnnotatedObject(new Main()).name(Environment.getCommand());
|
||||
consumer.accept(spec);
|
||||
|
||||
|
@ -679,11 +680,15 @@ public final class Picocli {
|
|||
cmd.setParameterExceptionHandler(new ShortErrorMessageHandler());
|
||||
cmd.setHelpFactory(new HelpFactory());
|
||||
cmd.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST, new SubCommandListRenderer());
|
||||
cmd.setErr(new PrintWriter(System.err, true));
|
||||
cmd.setErr(getErrWriter());
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
protected PrintWriter getErrWriter() {
|
||||
return new PrintWriter(System.err, true);
|
||||
}
|
||||
|
||||
private static void addHelp(CommandSpec currentSpec) {
|
||||
try {
|
||||
currentSpec.addOption(OptionSpec.builder(Help.OPTION_NAMES)
|
||||
|
@ -795,7 +800,6 @@ public final class Picocli {
|
|||
return mapper.getExpectedValues().iterator();
|
||||
}
|
||||
})
|
||||
.parameterConsumer(PropertyMapperParameterConsumer.INSTANCE)
|
||||
.hidden(mapper.isHidden());
|
||||
|
||||
if (mapper.getDefaultValue().isPresent()) {
|
||||
|
@ -804,6 +808,14 @@ public final class Picocli {
|
|||
|
||||
if (mapper.getType() != null) {
|
||||
optBuilder.type(mapper.getType());
|
||||
if (mapper.isList()) {
|
||||
// make picocli aware of the only list convention we allow
|
||||
optBuilder.splitRegex(",");
|
||||
} else if (mapper.getType().isEnum()) {
|
||||
// prevent the auto-conversion that picocli does
|
||||
// we validate the expected values later
|
||||
optBuilder.type(String.class);
|
||||
}
|
||||
} else {
|
||||
optBuilder.type(String.class);
|
||||
}
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* 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 org.keycloak.quarkus.runtime.cli;
|
||||
|
||||
import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX;
|
||||
|
||||
import java.util.Stack;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Model.ArgSpec;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
import picocli.CommandLine.Model.OptionSpec;
|
||||
import picocli.CommandLine.ParameterException;
|
||||
|
||||
public final class PropertyMapperParameterConsumer implements CommandLine.IParameterConsumer {
|
||||
|
||||
static final CommandLine.IParameterConsumer INSTANCE = new PropertyMapperParameterConsumer();
|
||||
|
||||
private PropertyMapperParameterConsumer() {
|
||||
// singleton
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consumeParameters(Stack<String> args, ArgSpec argSpec,
|
||||
CommandSpec commandSpec) {
|
||||
if (argSpec instanceof OptionSpec) {
|
||||
validateOption(args, argSpec, commandSpec);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateOption(Stack<String> args, ArgSpec argSpec, CommandSpec commandSpec) {
|
||||
OptionSpec option = (OptionSpec) argSpec;
|
||||
String name = String.join(", ", option.names());
|
||||
CommandLine commandLine = commandSpec.commandLine();
|
||||
|
||||
if (args.isEmpty() || !isOptionValue(args.peek())) {
|
||||
throw new ParameterException(commandLine,
|
||||
"Missing required value. " + getExpectedMessage(argSpec, option, name));
|
||||
}
|
||||
|
||||
// consumes the value, actual value validation will be performed later
|
||||
args.pop();
|
||||
|
||||
if (!args.isEmpty() && isOptionValue(args.peek())) {
|
||||
throw new ParameterException(commandLine, getExpectedMessage(argSpec, option, name));
|
||||
}
|
||||
}
|
||||
|
||||
private String getExpectedMessage(ArgSpec argSpec, OptionSpec option, String name) {
|
||||
return String.format("Option '%s' (%s) expects %s.%s", name, argSpec.paramLabel(),
|
||||
option.typeInfo().isMultiValue() ? "one or more comma separated values without whitespace": "a single value",
|
||||
getExpectedValuesMessage(argSpec.completionCandidates(), option.completionCandidates()));
|
||||
}
|
||||
|
||||
private boolean isOptionValue(String arg) {
|
||||
return !(arg.startsWith(ARG_PREFIX) || arg.startsWith(Picocli.ARG_SHORT_PREFIX));
|
||||
}
|
||||
|
||||
public static String getExpectedValuesMessage(Iterable<String> specCandidates, Iterable<String> optionCandidates) {
|
||||
return optionCandidates.iterator().hasNext() ? " Expected values are: " + String.join(", ", specCandidates) : "";
|
||||
}
|
||||
|
||||
}
|
|
@ -1,26 +1,28 @@
|
|||
package org.keycloak.quarkus.runtime.cli;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
|
||||
import org.keycloak.quarkus.runtime.cli.command.Start;
|
||||
import org.keycloak.quarkus.runtime.configuration.KcUnmatchedArgumentException;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.IParameterExceptionHandler;
|
||||
import picocli.CommandLine.MissingParameterException;
|
||||
import picocli.CommandLine.Model.ArgSpec;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
import picocli.CommandLine.Model.OptionSpec;
|
||||
import picocli.CommandLine.ParameterException;
|
||||
import picocli.CommandLine.UnmatchedArgumentException;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG;
|
||||
|
||||
public class ShortErrorMessageHandler implements IParameterExceptionHandler {
|
||||
|
||||
@Override
|
||||
|
@ -70,6 +72,15 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (ex instanceof MissingParameterException) {
|
||||
MissingParameterException mpe = (MissingParameterException)ex;
|
||||
if (mpe.getMissing().size() == 1) {
|
||||
ArgSpec spec = mpe.getMissing().get(0);
|
||||
if (spec instanceof OptionSpec) {
|
||||
OptionSpec option = (OptionSpec)spec;
|
||||
errorMessage = getExpectedMessage(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.println(cmd.getColorScheme().errorText(errorMessage));
|
||||
|
@ -97,4 +108,15 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
|
|||
private String[] getUnmatchedPartsByOptionSeparator(UnmatchedArgumentException uae, String separator) {
|
||||
return uae.getUnmatched().get(0).split(separator);
|
||||
}
|
||||
|
||||
private String getExpectedMessage(OptionSpec option) {
|
||||
return String.format("Option '%s' (%s) expects %s.%s", String.join(", ", option.names()), option.paramLabel(),
|
||||
option.typeInfo().isMultiValue() ? "one or more comma separated values without whitespace": "a single value",
|
||||
getExpectedValuesMessage(option.completionCandidates()));
|
||||
}
|
||||
|
||||
public static String getExpectedValuesMessage(Iterable<String> specCandidates) {
|
||||
return specCandidates.iterator().hasNext() ? " Expected values are: " + String.join(", ", specCandidates) : "";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@ import static org.keycloak.quarkus.runtime.configuration.Configuration.getRawPer
|
|||
|
||||
public abstract class AbstractStartCommand extends AbstractCommand implements Runnable {
|
||||
public static final String OPTIMIZED_BUILD_OPTION_LONG = "--optimized";
|
||||
|
||||
private boolean skipStart;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
@ -48,7 +50,9 @@ public abstract class AbstractStartCommand extends AbstractCommand implements Ru
|
|||
executionError(spec.commandLine(), Messages.optimizedUsedForFirstStartup());
|
||||
}
|
||||
|
||||
KeycloakMain.start((ExecutionExceptionHandler) cmd.getExecutionExceptionHandler(), cmd.getErr(), cmd.getParseResult().originalArgs().toArray(new String[0]));
|
||||
if (!skipStart) {
|
||||
KeycloakMain.start((ExecutionExceptionHandler) cmd.getExecutionExceptionHandler(), cmd.getErr(), cmd.getParseResult().originalArgs().toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
|
||||
protected void doBeforeRun() {
|
||||
|
@ -68,5 +72,9 @@ public abstract class AbstractStartCommand extends AbstractCommand implements Ru
|
|||
protected EnumSet<OptionCategory> excludedCategories() {
|
||||
return EnumSet.of(OptionCategory.IMPORT, OptionCategory.EXPORT);
|
||||
}
|
||||
|
||||
public void setSkipStart(boolean skipStart) {
|
||||
this.skipStart = skipStart;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ import org.keycloak.config.Option;
|
|||
import org.keycloak.config.OptionBuilder;
|
||||
import org.keycloak.config.OptionCategory;
|
||||
import org.keycloak.quarkus.runtime.cli.PropertyException;
|
||||
import org.keycloak.quarkus.runtime.cli.PropertyMapperParameterConsumer;
|
||||
import org.keycloak.quarkus.runtime.cli.ShortErrorMessageHandler;
|
||||
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
|
||||
import org.keycloak.quarkus.runtime.configuration.KcEnvConfigSource;
|
||||
import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider;
|
||||
|
@ -417,11 +417,15 @@ public class PropertyMapper<T> {
|
|||
validator.accept(this, value);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isList() {
|
||||
return getOption().getType() == java.util.List.class;
|
||||
}
|
||||
|
||||
public void validateValues(ConfigValue configValue, BiConsumer<ConfigValue, String> singleValidator) {
|
||||
String value = configValue.getValue();
|
||||
|
||||
boolean multiValued = getOption().getType() == java.util.List.class;
|
||||
boolean multiValued = isList();
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
String[] values = multiValued ? value.split(",") : new String[] { value };
|
||||
|
@ -462,10 +466,10 @@ public class PropertyMapper<T> {
|
|||
if (!expectedValues.isEmpty() && !expectedValues.contains(v) && getOption().isStrictExpectedValues()) {
|
||||
throw new PropertyException(
|
||||
String.format("Invalid value for option %s: %s.%s", getOptionAndSourceMessage(configValue), v,
|
||||
PropertyMapperParameterConsumer.getExpectedValuesMessage(expectedValues, expectedValues)));
|
||||
ShortErrorMessageHandler.getExpectedValuesMessage(expectedValues)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
String getOptionAndSourceMessage(ConfigValue configValue) {
|
||||
if (isCliOption(configValue)) {
|
||||
return String.format("'%s'", this.getCliFormat());
|
||||
|
|
|
@ -39,12 +39,17 @@ import static org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourcePro
|
|||
public final class PropertyMappers {
|
||||
|
||||
public static String VALUE_MASK = "*******";
|
||||
private static final MappersConfig MAPPERS = new MappersConfig();
|
||||
private static MappersConfig MAPPERS;
|
||||
private static final Logger log = Logger.getLogger(PropertyMappers.class);
|
||||
|
||||
private PropertyMappers(){}
|
||||
|
||||
|
||||
static {
|
||||
reset();
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
MAPPERS = new MappersConfig();
|
||||
MAPPERS.addAll(CachingPropertyMappers.getClusteringPropertyMappers());
|
||||
MAPPERS.addAll(DatabasePropertyMappers.getDatabasePropertyMappers());
|
||||
MAPPERS.addAll(HostnameV2PropertyMappers.getHostnamePropertyMappers());
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* 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 or.keycloak.quarkus.runtime.cli;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.quarkus.runtime.cli.Picocli;
|
||||
import org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand;
|
||||
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
|
||||
import org.keycloak.quarkus.runtime.configuration.test.AbstractConfigurationTest;
|
||||
|
||||
import io.smallrye.config.SmallRyeConfig;
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Help;
|
||||
|
||||
public class PicocliTest extends AbstractConfigurationTest {
|
||||
|
||||
// TODO: could utilize CLIResult
|
||||
private class NonRunningPicocli extends Picocli {
|
||||
|
||||
final StringWriter err = new StringWriter();
|
||||
SmallRyeConfig config;
|
||||
int exitCode = Integer.MAX_VALUE;
|
||||
|
||||
String getErrString() {
|
||||
// normalize line endings - TODO: could also normalize non-printable chars
|
||||
// but for now those are part of the expected output
|
||||
return System.lineSeparator().equals("\n") ? err.toString()
|
||||
: err.toString().replace(System.lineSeparator(), "\n");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PrintWriter getErrWriter() {
|
||||
return new PrintWriter(err, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void exitOnFailure(int exitCode, CommandLine cmd) {
|
||||
this.exitCode = exitCode;
|
||||
}
|
||||
|
||||
protected int runReAugmentationIfNeeded(List<String> cliArgs, CommandLine cmd, CommandLine currentCommand) {
|
||||
throw new AssertionError("Should not reaugment");
|
||||
};
|
||||
|
||||
@Override
|
||||
protected int run(CommandLine cmd, String[] argArray) {
|
||||
skipStart(cmd);
|
||||
return super.run(cmd, argArray);
|
||||
}
|
||||
|
||||
private void skipStart(CommandLine cmd) {
|
||||
for (CommandLine sub : cmd.getSubcommands().values()) {
|
||||
if (sub.getCommand() instanceof AbstractStartCommand) {
|
||||
((AbstractStartCommand) (sub.getCommand())).setSkipStart(true);
|
||||
}
|
||||
skipStart(sub);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseAndRun(List<String> cliArgs) {
|
||||
ConfigArgsConfigSource.setCliArgs(cliArgs.toArray(String[]::new));
|
||||
config = createConfig();
|
||||
super.parseAndRun(cliArgs);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
NonRunningPicocli pseudoLaunch(String... args) {
|
||||
NonRunningPicocli nonRunningPicocli = new NonRunningPicocli();
|
||||
nonRunningPicocli.parseAndRun(Arrays.asList(args));
|
||||
return nonRunningPicocli;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNegativeArgument() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev");
|
||||
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
|
||||
assertEquals("1h",
|
||||
nonRunningPicocli.config.getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue());
|
||||
|
||||
nonRunningPicocli = pseudoLaunch("start-dev", "--https-certificates-reload-period=-1");
|
||||
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
|
||||
assertNull(nonRunningPicocli.config.getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidArgumentType() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--http-port=a");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertThat(nonRunningPicocli.getErrString(),
|
||||
containsString("Invalid value for option '--http-port': 'a' is not an int"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failWrongEnumValue() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--log-console-level=wrong");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertThat(nonRunningPicocli.getErrString(), containsString(
|
||||
"Invalid value for option '--log-console-level': wrong. Expected values are: off, fatal, error, warn, info, debug, trace, all"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failMissingOptionValue() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--db");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertThat(nonRunningPicocli.getErrString(), containsString(
|
||||
"Option '--db' (vendor) expects a single value. Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failMultipleOptionValue() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("build", "--db", "mysql", "postgres");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertThat(nonRunningPicocli.getErrString(), containsString("Unknown option: 'postgres'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failMultipleMultiOptionValue() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("build", "--features", "linkedin-oauth", "account3");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertThat(nonRunningPicocli.getErrString(), containsString("Unknown option: 'account3'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failMissingMultiOptionValue() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("build", "--features");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertThat(nonRunningPicocli.getErrString(), containsString(
|
||||
"Option '--features' (feature) expects one or more comma separated values without whitespace. Expected values are:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failInvalidMultiOptionValue() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("build", "--features", "xyz,account3");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertThat(nonRunningPicocli.getErrString(),
|
||||
containsString("xyz is an unrecognized feature, it should be one of"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failUnknownOption() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("build", "--nosuch");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertThat(nonRunningPicocli.getErrString(), containsString("Unknown option: '--nosuch'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failUnknownOptionWhitespaceSeparatorNotShowingValue() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start", "--db-pasword", "mytestpw");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertThat(nonRunningPicocli.getErrString(), containsString(Help.defaultColorScheme(Help.Ansi.AUTO)
|
||||
.errorText("Unknown option: '--db-pasword'")
|
||||
+ "\nPossible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-driver, --db"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failUnknownOptionEqualsSeparatorNotShowingValue() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start", "--db-pasword=mytestpw");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertThat(nonRunningPicocli.getErrString(), containsString(Help.defaultColorScheme(Help.Ansi.AUTO)
|
||||
.errorText("Unknown option: '--db-pasword'")
|
||||
+ "\nPossible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-driver, --db"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failWithFirstOptionOnMultipleUnknownOptions() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start", "--db-username=foobar", "--db-pasword=mytestpw",
|
||||
"--foobar=barfoo");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertThat(nonRunningPicocli.getErrString(), containsString(Help.defaultColorScheme(Help.Ansi.AUTO)
|
||||
.errorText("Unknown option: '--db-pasword'")
|
||||
+ "\nPossible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-driver, --db"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failSingleParamWithSpace() {
|
||||
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start", "--db postgres");
|
||||
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
|
||||
assertThat(nonRunningPicocli.getErrString(), containsString(
|
||||
"Option: '--db postgres' is not expected to contain whitespace, please remove any unnecessary quoting/escaping"));
|
||||
}
|
||||
|
||||
}
|
|
@ -29,6 +29,7 @@ import org.keycloak.Config;
|
|||
import org.keycloak.quarkus.runtime.configuration.Configuration;
|
||||
import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider;
|
||||
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
|
@ -109,6 +110,7 @@ public abstract class AbstractConfigurationTest {
|
|||
}
|
||||
|
||||
SmallRyeConfigProviderResolver.class.cast(ConfigProviderResolver.instance()).releaseConfig(ConfigProvider.getConfig());
|
||||
PropertyMappers.reset();
|
||||
}
|
||||
|
||||
protected Config.Scope initConfig(String... scope) {
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* 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 org.keycloak.it.cli;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.it.junit5.extension.CLIResult;
|
||||
import org.keycloak.it.junit5.extension.CLITest;
|
||||
import org.keycloak.it.junit5.extension.ConfigurationTestResource;
|
||||
|
||||
import io.quarkus.test.common.QuarkusTestResource;
|
||||
import io.quarkus.test.junit.main.Launch;
|
||||
import io.quarkus.test.junit.main.LaunchResult;
|
||||
import org.keycloak.it.utils.KeycloakDistribution;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@QuarkusTestResource(value = ConfigurationTestResource.class, restrictToAnnotatedClass = true)
|
||||
@CLITest
|
||||
public class OptionValidationTest {
|
||||
|
||||
@Test
|
||||
@Launch({"build", "--db"})
|
||||
public void failMissingOptionValue(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertThat(cliResult.getErrorOutput(), containsString("Missing required value. Option '--db' (vendor) expects a single value. Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"build", "--db", "mysql", "postgres"})
|
||||
public void failMultipleOptionValue(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertThat(cliResult.getErrorOutput(), containsString("Option '--db' (vendor) expects a single value. Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"build", "--features", "linkedin-oauth", "account3"})
|
||||
public void failMultipleMultiOptionValue(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertThat(cliResult.getErrorOutput(), containsString("Option '--features' (feature) expects one or more comma separated values without whitespace. Expected values are: "));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"build", "--features", "xyz,account3"})
|
||||
public void failInvalidMultiOptionValue(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertThat(cliResult.getErrorOutput(), containsString("xyz is an unrecognized feature, it should be one of"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"build", "--nosuch"})
|
||||
public void failUnknownOption(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertEquals("Unknown option: '--nosuch'\n" +
|
||||
"Try '" + KeycloakDistribution.SCRIPT_CMD + " build --help' for more information on the available options.", cliResult.getErrorOutput());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"start", "--db-pasword", "mytestpw"})
|
||||
public void failUnknownOptionWhitespaceSeparatorNotShowingValue(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertEquals("Unknown option: '--db-pasword'\n" +
|
||||
"Possible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-driver, --db\n" +
|
||||
"Try '" + KeycloakDistribution.SCRIPT_CMD + " start --help' for more information on the available options.", cliResult.getErrorOutput());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"start", "--db-pasword=mytestpw"})
|
||||
public void failUnknownOptionEqualsSeparatorNotShowingValue(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertEquals("Unknown option: '--db-pasword'\n" +
|
||||
"Possible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-driver, --db\n" +
|
||||
"Try '" + KeycloakDistribution.SCRIPT_CMD + " start --help' for more information on the available options.", cliResult.getErrorOutput());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"start", "--db-username=foobar", "--db-pasword=mytestpw", "--foobar=barfoo"})
|
||||
public void failWithFirstOptionOnMultipleUnknownOptions(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
assertEquals("Unknown option: '--db-pasword'\n" +
|
||||
"Possible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-driver, --db\n" +
|
||||
"Try '" + KeycloakDistribution.SCRIPT_CMD + " start --help' for more information on the available options.", cliResult.getErrorOutput());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({"start", "--db postgres"})
|
||||
void failSingleParamWithSpace(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
cliResult.assertError("Option: '--db postgres' is not expected to contain whitespace, please remove any unnecessary quoting/escaping");
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue