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:
Steven Hawkins 2024-09-27 11:36:05 -04:00 committed by GitHub
parent b717810061
commit d981f7f55d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 299 additions and 217 deletions

View file

@ -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>

View file

@ -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);
}
/**

View file

@ -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);
}

View file

@ -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) : "";
}
}

View file

@ -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) : "";
}
}

View file

@ -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;
}
}

View file

@ -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());

View file

@ -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());

View file

@ -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"));
}
}

View file

@ -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) {

View file

@ -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");
}
}