diff --git a/quarkus/runtime/pom.xml b/quarkus/runtime/pom.xml index ab56c8dfb11..0155a9030d5 100644 --- a/quarkus/runtime/pom.xml +++ b/quarkus/runtime/pom.xml @@ -615,6 +615,12 @@ junit test + + + org.hamcrest + hamcrest + test + diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java index d832958ba48..f507ba80cec 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java @@ -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); } /** diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java index 53b87db7b4f..bdd5f995d4a 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java @@ -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 cliArgs) { + public void parseAndRun(List 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 cliArgs, List commandLineList) { + protected int run(CommandLine cmd, String[] argArray) { + return cmd.execute(argArray); + } + + private CommandLine createCommandLineForCommand(List cliArgs, List 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 cliArgs, CommandLine cmd, CommandLine currentCommand) { + protected int runReAugmentationIfNeeded(List 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 consumer) { + public CommandLine createCommandLine(Consumer 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); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/PropertyMapperParameterConsumer.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/PropertyMapperParameterConsumer.java deleted file mode 100644 index 49c6980ed68..00000000000 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/PropertyMapperParameterConsumer.java +++ /dev/null @@ -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 args, ArgSpec argSpec, - CommandSpec commandSpec) { - if (argSpec instanceof OptionSpec) { - validateOption(args, argSpec, commandSpec); - } - } - - private void validateOption(Stack 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 specCandidates, Iterable optionCandidates) { - return optionCandidates.iterator().hasNext() ? " Expected values are: " + String.join(", ", specCandidates) : ""; - } - -} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/ShortErrorMessageHandler.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/ShortErrorMessageHandler.java index 78ec05a4016..251826336d9 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/ShortErrorMessageHandler.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/ShortErrorMessageHandler.java @@ -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 specCandidates) { + return specCandidates.iterator().hasNext() ? " Expected values are: " + String.join(", ", specCandidates) : ""; + } + } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java index 91bf436eeea..43b942cab5f 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java @@ -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 excludedCategories() { return EnumSet.of(OptionCategory.IMPORT, OptionCategory.EXPORT); } + + public void setSkipStart(boolean skipStart) { + this.skipStart = skipStart; + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java index a21152242b1..4bab8ba690d 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java @@ -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 { validator.accept(this, value); } } + + public boolean isList() { + return getOption().getType() == java.util.List.class; + } public void validateValues(ConfigValue configValue, BiConsumer 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 { 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()); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java index bbeb0dc6084..9f7b3fb5922 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java @@ -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()); diff --git a/quarkus/runtime/src/test/java/or/keycloak/quarkus/runtime/cli/PicocliTest.java b/quarkus/runtime/src/test/java/or/keycloak/quarkus/runtime/cli/PicocliTest.java new file mode 100644 index 00000000000..6a6546be7d6 --- /dev/null +++ b/quarkus/runtime/src/test/java/or/keycloak/quarkus/runtime/cli/PicocliTest.java @@ -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 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 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")); + } + +} diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/AbstractConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/AbstractConfigurationTest.java index 47cf4f527d9..8fcc28aefd8 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/AbstractConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/AbstractConfigurationTest.java @@ -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) { diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/OptionValidationTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/OptionValidationTest.java deleted file mode 100644 index 5139014e9db..00000000000 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/OptionValidationTest.java +++ /dev/null @@ -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"); - } -}