From e1916fbdb153cb0053fa1e50ccc9164537b2dba0 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Tue, 23 Nov 2021 11:45:46 -0300 Subject: [PATCH] [KEYCLOAK-19311] - Baseline for Dist.X tests --- .github/workflows/ci.yml | 46 +++ distribution/server-x-dist/assembly.xml | 2 +- .../org/keycloak/quarkus/StartupTest.java | 28 -- .../KeycloakNegativeHealthCheckTest.java | 1 - .../health/KeycloakReadyHealthCheckTest.java | 1 - .../src/test/resources/application.properties | 9 - quarkus/pom.xml | 1 + .../keycloak/quarkus/runtime/Environment.java | 18 + .../quarkus/runtime/KeycloakMain.java | 26 +- .../cli/ExecutionExceptionHandler.java | 3 - .../keycloak/quarkus/runtime/cli/Picocli.java | 12 +- .../quarkus/runtime/cli/command/Build.java | 8 +- .../quarkus/runtime/cli/command/Main.java | 3 +- .../runtime/cli/command/ShowConfig.java | 5 +- .../configuration/ConfigArgsConfigSource.java | 14 +- .../KeycloakConfigSourceProvider.java | 39 +- .../KeycloakPropertiesConfigSource.java | 150 ++++---- .../mappers/DatabasePropertyMappers.java | 6 +- .../runtime/integration/QuarkusPlatform.java | 11 +- .../storage/infinispan/CacheInitializer.java | 1 - .../resources/META-INF/keycloak.properties | 0 .../src/main/resources/application.properties | 26 ++ .../src/main/resources/application.properties | 29 +- quarkus/tests/integration/pom.xml | 65 ++++ .../it/junit5/extension/CLIResult.java | 107 ++++++ .../keycloak/it/junit5/extension/CLITest.java | 30 ++ .../it/junit5/extension/CLITestExtension.java | 172 +++++++++ .../it/junit5/extension/DistributionTest.java | 42 +++ .../it/utils/KeycloakDistribution.java | 337 ++++++++++++++++++ .../java/org/keycloak/it/utils/Maven.java | 80 +++++ .../org/keycloak/it/cli/HelpCommandTest.java | 59 +++ .../keycloak/it/cli/OptionValidationTest.java | 49 +++ .../it/cli/ShowConfigCommandTest.java | 47 +++ .../org/keycloak/it/cli/StartCommandTest.java | 63 ++++ .../keycloak/it/cli/StartDevCommandTest.java | 43 +++ .../it/cli/dist/BuildCommandDistTest.java | 47 +++ .../it/cli/dist/HelpCommandDistTest.java | 25 ++ .../keycloak/it/cli/dist/MetricsDistTest.java | 48 +++ .../it/cli/dist/StartCommandDistTest.java | 25 ++ .../it/cli/dist/StartDevCommandDistTest.java | 25 ++ quarkus/tests/pom.xml | 39 ++ 41 files changed, 1552 insertions(+), 190 deletions(-) delete mode 100644 quarkus/deployment/src/test/java/test/org/keycloak/quarkus/StartupTest.java delete mode 100644 quarkus/deployment/src/test/resources/application.properties rename quarkus/{server => runtime}/src/main/resources/META-INF/keycloak.properties (100%) create mode 100644 quarkus/runtime/src/main/resources/application.properties create mode 100644 quarkus/tests/integration/pom.xml create mode 100644 quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLIResult.java create mode 100644 quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITest.java create mode 100644 quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java create mode 100644 quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/DistributionTest.java create mode 100644 quarkus/tests/integration/src/main/java/org/keycloak/it/utils/KeycloakDistribution.java create mode 100644 quarkus/tests/integration/src/main/java/org/keycloak/it/utils/Maven.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/HelpCommandTest.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/OptionValidationTest.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/ShowConfigCommandTest.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/StartCommandTest.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/StartDevCommandTest.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildCommandDistTest.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HelpCommandDistTest.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/MetricsDistTest.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartDevCommandDistTest.java create mode 100644 quarkus/tests/pom.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71c91a737c..802e4d8fdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -373,6 +373,52 @@ jobs: path: reports-quarkus-cluster-tests.zip if-no-files-found: ignore + ### Tests: Quarkus distribution + + quarkus-tests: + name: Quarkus Tests + needs: build + runs-on: ubuntu-latest + env: + MAVEN_OPTS: -Xmx1024m + steps: + - uses: actions/checkout@v2 + - name: Cache Maven packages + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: cache-1-${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: cache-1-${{ runner.os }}-m2 + - name: Cleanup org.keycloak artifacts + run: rm -rf ~/.m2/repository/org/keycloak >/dev/null || true + + - name: Download built keycloak + id: download-keycloak + uses: actions/download-artifact@v2 + with: + path: ~/.m2/repository/org/keycloak/ + name: keycloak-artifacts.zip + - uses: actions/setup-java@v1 + with: + java-version: ${{ env.DEFAULT_JDK_VERSION }} + - name: Update maven settings + run: mkdir -p ~/.m2 ; cp .github/settings.xml ~/.m2/ + - name: Run Quarkus Tests + run: | + mvn clean install -nsu -B -f quarkus/tests/pom.xml | misc/log/trimmer.sh + TEST_RESULT=${PIPESTATUS[0]} + find . -path '*/target/surefire-reports/*.xml' | zip -q reports-quarkus-tests.zip -@ + exit $TEST_RESULT + + - name: Quarkus test reports + uses: actions/upload-artifact@v2 + if: failure() + with: + name: reports-quarkus-tests + retention-days: 14 + path: reports-quarkus-tests.zip + if-no-files-found: ignore + quickstarts-tests: name: Quickstarts Tests needs: build diff --git a/distribution/server-x-dist/assembly.xml b/distribution/server-x-dist/assembly.xml index 6a25231d39..0658b162e0 100755 --- a/distribution/server-x-dist/assembly.xml +++ b/distribution/server-x-dist/assembly.xml @@ -102,7 +102,7 @@ conf - target/keycloak-quarkus-server-app/META-INF/keycloak.properties + ../../quarkus/runtime/src/main/resources/META-INF/keycloak.properties conf diff --git a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/StartupTest.java b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/StartupTest.java deleted file mode 100644 index 47a1ecd4d5..0000000000 --- a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/StartupTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package test.org.keycloak.quarkus; - -import org.hamcrest.Matchers; -import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.spec.JavaArchive; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import io.quarkus.test.QuarkusUnitTest; -import io.restassured.RestAssured; - -public class StartupTest { - - @RegisterExtension - static final QuarkusUnitTest test = new QuarkusUnitTest() - .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addAsResource("application.properties", "application.properties") - .addAsResource("keycloak.properties", "META-INF/keycloak.properties")); - - @Test - public void testWelcomePage() throws InterruptedException { - RestAssured.given() - .when().get("/") - .then() - .statusCode(200) - .body(Matchers.containsString("Please create an initial admin user to get started")); - } -} diff --git a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakNegativeHealthCheckTest.java b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakNegativeHealthCheckTest.java index 01a3ee3540..b857515294 100644 --- a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakNegativeHealthCheckTest.java +++ b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakNegativeHealthCheckTest.java @@ -36,7 +36,6 @@ public class KeycloakNegativeHealthCheckTest { @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addAsResource("application.properties", "application.properties") .addAsResource("keycloak.properties", "META-INF/keycloak.properties")); @Test diff --git a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakReadyHealthCheckTest.java b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakReadyHealthCheckTest.java index 4eb6520779..0600d12a2b 100644 --- a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakReadyHealthCheckTest.java +++ b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/KeycloakReadyHealthCheckTest.java @@ -33,7 +33,6 @@ public class KeycloakReadyHealthCheckTest { @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addAsResource("application.properties", "application.properties") .addAsResource("keycloak.properties", "META-INF/keycloak.properties")); @Test diff --git a/quarkus/deployment/src/test/resources/application.properties b/quarkus/deployment/src/test/resources/application.properties deleted file mode 100644 index d795638887..0000000000 --- a/quarkus/deployment/src/test/resources/application.properties +++ /dev/null @@ -1,9 +0,0 @@ -quarkus.http.root-path=/ -quarkus.application.name=Keycloak -quarkus.banner.enabled=false - -quarkus.resteasy.ignore-application-classes=true -quarkus.arc.ignored-split-packages=org.keycloak.* - -# we do not want running testcontainers when running tests in this module -quarkus.devservices.enabled=false \ No newline at end of file diff --git a/quarkus/pom.xml b/quarkus/pom.xml index bb53a03953..513738977a 100644 --- a/quarkus/pom.xml +++ b/quarkus/pom.xml @@ -159,6 +159,7 @@ runtime deployment server + tests diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java index 228a31ce2a..a07a3e0cbb 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java @@ -41,6 +41,7 @@ public final class Environment { public static final String DEFAULT_THEMES_PATH = "/themes"; public static final String DEV_PROFILE_VALUE = "dev"; public static final String USER_INVOKED_CLI_COMMAND = "picocli.invoked.command"; + public static final String LAUNCH_MODE = "kc.launch.mode"; private Environment() {} @@ -90,6 +91,7 @@ public final class Environment { if (isWindows()) { return "kc.bat"; } + return "kc.sh"; } @@ -133,6 +135,10 @@ public final class Environment { public static void setProfile(String profile) { System.setProperty(PROFILE, profile); System.setProperty("quarkus.profile", profile); + if (isTestLaunchMode()) { + System.setProperty("mp.config.profile", profile); + System.setProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP, profile); + } } public static String getProfileOrDefault(String defaultProfile) { @@ -193,4 +199,16 @@ public final class Environment { public static boolean isQuarkusDevMode() { return ProfileManager.getLaunchMode().equals(LaunchMode.DEVELOPMENT); } + + public static boolean isTestLaunchMode() { + return "test".equals(System.getProperty(LAUNCH_MODE)); + } + + public static void forceTestLaunchMode() { + System.setProperty(LAUNCH_MODE, "test"); + } + + public static boolean isDistribution() { + return Environment.getCommand().startsWith("kc."); + } } 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 f5a276553d..655a5f2a69 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 @@ -19,12 +19,15 @@ package org.keycloak.quarkus.runtime; import static org.keycloak.quarkus.runtime.Environment.isDevProfile; import static org.keycloak.quarkus.runtime.Environment.getProfileOrDefault; +import static org.keycloak.quarkus.runtime.Environment.isTestLaunchMode; import static org.keycloak.quarkus.runtime.cli.Picocli.parseAndRun; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.enterprise.context.ApplicationScoped; + import io.quarkus.runtime.ApplicationLifecycleManager; import io.quarkus.runtime.Quarkus; @@ -41,6 +44,7 @@ import picocli.CommandLine; *

The main entry point, responsible for initialize and run the CLI as well as start the server. */ @QuarkusMain(name = "keycloak") +@ApplicationScoped public class KeycloakMain implements QuarkusApplication { private static final Logger LOGGER = Logger.getLogger(KeycloakMain.class); @@ -61,7 +65,7 @@ public class KeycloakMain implements QuarkusApplication { public static void start(CommandLine cmd) { try { - Quarkus.run(KeycloakMain.class, (integer, cause) -> { + Quarkus.run(KeycloakMain.class, (exitCode, cause) -> { if (cause != null) { ExecutionExceptionHandler exceptionHandler = (ExecutionExceptionHandler) cmd.getExecutionExceptionHandler(); @@ -69,6 +73,12 @@ public class KeycloakMain implements QuarkusApplication { String.format("Failed to start server using profile (%s)", getProfileOrDefault("prod")), cause.getCause()); } + + if (Environment.isDistribution()) { + // assume that it is running the distribution + // as we are replacing the default exit handler, we need to force exit + System.exit(exitCode); + } }); } catch (Throwable cause) { ExecutionExceptionHandler exceptionHandler = (ExecutionExceptionHandler) cmd.getExecutionExceptionHandler(); @@ -87,7 +97,17 @@ public class KeycloakMain implements QuarkusApplication { if (isDevProfile()) { LOGGER.warnf("Running the server in dev mode. DO NOT use this configuration in production."); } - Quarkus.waitForExit(); - return ApplicationLifecycleManager.getExitCode(); + + int exitCode = ApplicationLifecycleManager.getExitCode(); + + if (isTestLaunchMode()) { + // in test mode we exit immediately + // we should be managing this behavior more dynamically depending on the tests requirements (short/long lived) + Quarkus.asyncExit(exitCode); + } else { + Quarkus.waitForExit(); + } + + return exitCode; } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/ExecutionExceptionHandler.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/ExecutionExceptionHandler.java index ed63c5eb86..dd5b3e7e10 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/ExecutionExceptionHandler.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/ExecutionExceptionHandler.java @@ -29,7 +29,6 @@ import org.keycloak.quarkus.runtime.InitializationException; import org.keycloak.quarkus.runtime.Messages; import org.keycloak.quarkus.runtime.integration.QuarkusPlatform; -import io.quarkus.runtime.Quarkus; import io.smallrye.config.ConfigValue; import picocli.CommandLine; import picocli.CommandLine.ParseResult; @@ -80,8 +79,6 @@ public final class ExecutionExceptionHandler implements CommandLine.IExecutionEx logError(errorWriter, "For more details run the same command passing the '--verbose' option. Also you can use '--help' to see the details about the usage of the particular command."); } } - - Quarkus.asyncExit(1); } private void dumpException(PrintWriter errorWriter, Throwable cause) { 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 d70eea2ece..2a381aa7d2 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 @@ -55,6 +55,7 @@ import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; import org.keycloak.quarkus.runtime.Environment; +import io.quarkus.runtime.Quarkus; import io.smallrye.config.ConfigValue; import picocli.CommandLine; import picocli.CommandLine.Model.CommandSpec; @@ -80,14 +81,7 @@ public final class Picocli { runReAugmentationIfNeeded(cliArgs, cmd); - int exitCode = cmd.execute(cliArgs.toArray(new String[0])); - - if (Environment.isQuarkusDevMode()) { - // do not exit if running in dev mode, otherwise quarkus dev mode will exit when running from IDE - return; - } - - System.exit(exitCode); + cmd.execute(cliArgs.toArray(new String[0])); } private static void runReAugmentationIfNeeded(List cliArgs, CommandLine cmd) { @@ -246,7 +240,7 @@ public final class Picocli { return key.startsWith("kc.provider.file"); } - private static CommandLine createCommandLine(List cliArgs) { + public static CommandLine createCommandLine(List cliArgs) { CommandSpec spec = CommandSpec.forAnnotatedObject(new Main()) .name(Environment.getCommand()); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java index b71cc0fd9d..853f4f3d26 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java @@ -30,6 +30,8 @@ import io.quarkus.runtime.configuration.ProfileManager; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; +import java.io.IOException; +import java.nio.file.Files; import java.util.List; @Command(name = Build.NAME, @@ -115,7 +117,11 @@ public final class Build extends AbstractCommand implements Runnable { private void cleanTempResources() { if (!ProfileManager.getLaunchMode().isDevOrTest()) { // only needed for dev/testing purposes - getHomePath().resolve("quarkus-artifact.properties").toFile().delete(); + try { + Files.delete(getHomePath().resolve("quarkus-artifact.properties")); + } catch (IOException cause) { + throw new RuntimeException("Failed to delete temporary resources", cause); + } } } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java index 741bbabd7c..0d67617560 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Main.java @@ -22,6 +22,7 @@ import static org.keycloak.quarkus.runtime.cli.Picocli.NO_PARAM_LABEL; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler; import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider; +import org.keycloak.quarkus.runtime.configuration.KeycloakPropertiesConfigSource; import picocli.CommandLine; import picocli.CommandLine.Command; @@ -104,6 +105,6 @@ public final class Main { description = "Set the path to a configuration file. By default, configuration properties are read from the \"keycloak.properties\" file in the \"conf\" directory.", paramLabel = "file") public void setConfigFile(String path) { - System.setProperty(KeycloakConfigSourceProvider.KEYCLOAK_CONFIG_FILE_PROP, path); + System.setProperty(KeycloakPropertiesConfigSource.KEYCLOAK_CONFIG_FILE_PROP, path); } } \ No newline at end of file diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java index b25a91d6b8..7271973b37 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java @@ -36,6 +36,7 @@ import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; import org.keycloak.quarkus.runtime.Environment; +import io.quarkus.runtime.Quarkus; import io.smallrye.config.ConfigValue; import picocli.CommandLine.Command; import picocli.CommandLine.Parameters; @@ -70,8 +71,8 @@ public final class ShowConfig extends AbstractCommand implements Runnable { .forEachOrdered(this::printProperty); } - if (!parseBoolean(System.getProperty("kc.show.config.runtime", Boolean.FALSE.toString()))) { - System.exit(0); + if (!Boolean.getBoolean("kc.show.config.runtime")) { + Quarkus.asyncExit(0); } } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java index f8889d6beb..50fb453636 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java @@ -17,6 +17,7 @@ package org.keycloak.quarkus.runtime.configuration; +import static org.keycloak.quarkus.runtime.Environment.isTestLaunchMode; import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_KEY_VALUE_SPLIT; import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX; import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_SPLIT; @@ -48,20 +49,29 @@ public class ConfigArgsConfigSource extends PropertiesConfigSource { private static final Logger log = Logger.getLogger(ConfigArgsConfigSource.class); + private final boolean alwaysParseArgs; + ConfigArgsConfigSource() { // higher priority over default Quarkus config sources super(parseArgument(), "CliConfigSource", 500); + alwaysParseArgs = isTestLaunchMode(); } @Override public String getValue(String propertyName) { - String value = super.getValue(propertyName); + Map properties = getProperties(); + + if (alwaysParseArgs) { + properties = parseArgument(); + } + + String value = properties.get(propertyName); if (value != null) { return value; } - return super.getValue(propertyName.replace('-', '.')); + return properties.get(propertyName.replace('-', '.')); } private static Map parseArgument() { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KeycloakConfigSourceProvider.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KeycloakConfigSourceProvider.java index 2c509d110c..45d2db7708 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KeycloakConfigSourceProvider.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KeycloakConfigSourceProvider.java @@ -17,9 +17,7 @@ package org.keycloak.quarkus.runtime.configuration; -import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX; -import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -34,8 +32,6 @@ public class KeycloakConfigSourceProvider implements ConfigSourceProvider { private static final Logger log = Logger.getLogger(KeycloakConfigSourceProvider.class); - public static final String KEYCLOAK_CONFIG_FILE_ENV = "KC_CONFIG_FILE"; - public static final String KEYCLOAK_CONFIG_FILE_PROP = NS_KEYCLOAK_PREFIX + "config.file"; private static final List CONFIG_SOURCES = new ArrayList<>(); public static PersistedConfigSource PERSISTED_CONFIG_SOURCE; @@ -57,14 +53,10 @@ public class KeycloakConfigSourceProvider implements ConfigSourceProvider { PERSISTED_CONFIG_SOURCE = new PersistedConfigSource(getPersistedConfigFile()); CONFIG_SOURCES.add(PERSISTED_CONFIG_SOURCE); - Path configFile = getConfigurationFile(); + CONFIG_SOURCES.addAll(new KeycloakPropertiesConfigSource.InFileSystem().getConfigSources(Thread.currentThread().getContextClassLoader())); - if (configFile != null) { - CONFIG_SOURCES.add(new KeycloakPropertiesConfigSource.InFileSystem(configFile)); - } else { - log.debug("Loading the default server configuration"); - CONFIG_SOURCES.add(new KeycloakPropertiesConfigSource.InJar()); - } + // by enabling this config source we are able to rely on the default settings when running tests + CONFIG_SOURCES.addAll(new KeycloakPropertiesConfigSource.InClassPath().getConfigSources(Thread.currentThread().getContextClassLoader())); } /** @@ -76,31 +68,6 @@ public class KeycloakConfigSourceProvider implements ConfigSourceProvider { initializeSources(); } - private static Path getConfigurationFile() { - String filePath = System.getProperty(KEYCLOAK_CONFIG_FILE_PROP); - - if (filePath == null) - filePath = System.getenv(KEYCLOAK_CONFIG_FILE_ENV); - - if (filePath == null) { - String homeDir = Environment.getHomeDir(); - - if (homeDir != null) { - File file = Paths.get(homeDir, "conf", KeycloakPropertiesConfigSource.KEYCLOAK_PROPERTIES).toFile(); - - if (file.exists()) { - filePath = file.getAbsolutePath(); - } - } - } - - if (filePath == null) { - return null; - } - - return Paths.get(filePath); - } - public static Path getPersistedConfigFile() { String homeDir = Environment.getHomeDir(); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KeycloakPropertiesConfigSource.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KeycloakPropertiesConfigSource.java index 0456513697..01a7209ce0 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KeycloakPropertiesConfigSource.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/KeycloakPropertiesConfigSource.java @@ -17,99 +17,125 @@ package org.keycloak.quarkus.runtime.configuration; -import java.io.Closeable; -import java.io.FileNotFoundException; -import java.io.IOError; +import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; +import java.net.URI; +import java.net.URL; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.regex.Pattern; -import org.jboss.logging.Logger; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.eclipse.microprofile.config.spi.ConfigSourceProvider; +import org.keycloak.quarkus.runtime.Environment; +import io.smallrye.config.AbstractLocationConfigSourceLoader; import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.common.utils.ConfigSourceUtil; import static org.keycloak.common.util.StringPropertyReplacer.replaceProperties; import static org.keycloak.quarkus.runtime.configuration.Configuration.getMappedPropertyName; import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK; +import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX; import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS; /** * A configuration source for {@code keycloak.properties}. */ -public abstract class KeycloakPropertiesConfigSource extends PropertiesConfigSource { - - private static final Logger log = Logger.getLogger(KeycloakPropertiesConfigSource.class); +public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSourceLoader { private static final Pattern DOT_SPLIT = Pattern.compile("\\."); - static final String KEYCLOAK_PROPERTIES = "keycloak.properties"; + private static final String KEYCLOAK_CONFIG_FILE_ENV = "KC_CONFIG_FILE"; + private static final String KEYCLOAK_PROPERTIES = "keycloak.properties"; + public static final String KEYCLOAK_CONFIG_FILE_PROP = NS_KEYCLOAK_PREFIX + "config.file"; - KeycloakPropertiesConfigSource(InputStream is, int ordinal) { - super(readProperties(is), KEYCLOAK_PROPERTIES, ordinal); + @Override + protected String[] getFileExtensions() { + return new String[] { "properties" }; } - private static Map readProperties(final InputStream is) { - if (is == null) { - return Collections.emptyMap(); - } - try (Closeable ignored = is) { - Properties properties = new Properties(); - properties.load(is); - return transform((Map) properties); - } catch (IOException e) { - throw new IOError(e); - } + @Override + protected ConfigSource loadConfigSource(URL url, int ordinal) throws IOException { + return new PropertiesConfigSource(transform(ConfigSourceUtil.urlToMap(url)), KEYCLOAK_PROPERTIES, ordinal); } - public static final class InJar extends KeycloakPropertiesConfigSource { - public InJar() { - super(openStream(), 250); + public static class InClassPath extends KeycloakPropertiesConfigSource implements ConfigSourceProvider { + + @Override + public List getConfigSources(final ClassLoader classLoader) { + return loadConfigSources("META-INF/keycloak.properties", 240, classLoader); } - private static InputStream openStream() { - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - if (cl == null) { - cl = KeycloakPropertiesConfigSource.class.getClassLoader(); - } - InputStream is; - String fileName = "META-INF/" + KEYCLOAK_PROPERTIES; - if (cl == null) { - is = ClassLoader.getSystemResourceAsStream(fileName); - } else { - is = cl.getResourceAsStream(fileName); - } - if (is != null) { - log.debug("Loading the server configuration from the classpath"); - } - return is; - } - } - - public static final class InFileSystem extends KeycloakPropertiesConfigSource { - - public InFileSystem(Path path) { - super(openStream(path), 250); - } - - private static InputStream openStream(Path path) { - if (path == null) { - throw new IllegalArgumentException("Configuration file path can not be null"); - } + @Override + protected List tryClassPath(URI uri, int ordinal, ClassLoader classLoader) { try { - log.debugf("Loading the server configuration from %s", path); - return Files.newInputStream(path); - } catch (NoSuchFileException | FileNotFoundException e) { - throw new IllegalArgumentException("Configuration file not found at [" + path + "]"); - } catch (IOException e) { - throw new RuntimeException("Unexpected error reading configuration file at [" + path + "]", e); + return super.tryClassPath(uri, ordinal, classLoader); + } catch (RuntimeException e) { + Throwable cause = e.getCause(); + + if (cause instanceof NoSuchFileException) { + // configuration step happens before classpath is updated, and it might happen that + // provider JARs are still in classpath index but removed from the providers dir + return Collections.emptyList(); + } + + throw e; } } + + @Override + protected List tryFileSystem(final URI uri, final int ordinal) { + return Collections.emptyList(); + } + } + + public static class InFileSystem extends KeycloakPropertiesConfigSource implements ConfigSourceProvider { + + @Override + public List getConfigSources(final ClassLoader classLoader) { + Path configFile = getConfigurationFile(); + + if (configFile == null) { + return Collections.emptyList(); + } + + return loadConfigSources(configFile.toUri().toString(), 250, classLoader); + } + + @Override + protected List tryClassPath(final URI uri, final int ordinal, final ClassLoader classLoader) { + return Collections.emptyList(); + } + + private Path getConfigurationFile() { + String filePath = System.getProperty(KEYCLOAK_CONFIG_FILE_PROP); + + if (filePath == null) + filePath = System.getenv(KEYCLOAK_CONFIG_FILE_ENV); + + if (filePath == null) { + String homeDir = Environment.getHomeDir(); + + if (homeDir != null) { + File file = Paths.get(homeDir, "conf", KeycloakPropertiesConfigSource.KEYCLOAK_PROPERTIES).toFile(); + + if (file.exists()) { + filePath = file.getAbsolutePath(); + } + } + } + + if (filePath == null) { + return null; + } + + return Paths.get(filePath); + } } private static Map transform(Map properties) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java index bd1f3cb0c4..85b398e24f 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java @@ -20,13 +20,13 @@ final class DatabasePropertyMappers { .mapFrom("db") .to("quarkus.hibernate-orm.dialect") .isBuildTimeProperty(true) - .transformer((db, context) -> Database.getDialect(db).orElse(null)) + .transformer((db, context) -> Database.getDialect(db).orElse(Database.getDialect("h2-file").get())) .hidden(true) .build(), builder().from("db-driver") .mapFrom("db") .to("quarkus.datasource.jdbc.driver") - .transformer((db, context) -> Database.getDriver(db).orElse(null)) + .transformer((db, context) -> Database.getDriver(db).orElse(Database.getDriver("h2-file").get())) .hidden(true) .build(), builder().from("db"). @@ -111,7 +111,7 @@ final class DatabasePropertyMappers { addInitializationException(invalidDatabaseVendor(db, Database.getAliases())); - return null; + return "h2"; }; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusPlatform.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusPlatform.java index ee14a30485..0b30375cad 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusPlatform.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusPlatform.java @@ -31,6 +31,8 @@ import org.keycloak.platform.PlatformProvider; import org.keycloak.quarkus.runtime.InitializationException; import org.keycloak.quarkus.runtime.Environment; +import io.quarkus.runtime.Quarkus; + public class QuarkusPlatform implements PlatformProvider { private static final Logger log = Logger.getLogger(QuarkusPlatform.class); @@ -56,6 +58,9 @@ public class QuarkusPlatform implements PlatformProvider { for (Throwable inner : platform.getDeferredExceptions()) { quarkusException.addSuppressed(inner); } + // reset this instance, mainly deferred exceptions, so that the subsequent starts do not fail due to previous errors + // this is mainly important when the server is running in test mode + platform.reset(); throw quarkusException; } } @@ -91,7 +96,7 @@ public class QuarkusPlatform implements PlatformProvider { @Override public void exit(Throwable cause) { - throw new RuntimeException(cause); + Quarkus.asyncExit(1); } /** @@ -162,4 +167,8 @@ public class QuarkusPlatform implements PlatformProvider { } return tmpDir; } + + private void reset() { + deferredExceptions.clear(); + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheInitializer.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheInitializer.java index e5d2b00d91..44dffc29e5 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheInitializer.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheInitializer.java @@ -21,7 +21,6 @@ import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; import org.infinispan.configuration.parsing.ParserRegistry; import org.infinispan.jboss.marshalling.core.JBossUserMarshaller; import org.infinispan.manager.DefaultCacheManager; -import org.jboss.logging.Logger; import org.keycloak.Config; public class CacheInitializer { diff --git a/quarkus/server/src/main/resources/META-INF/keycloak.properties b/quarkus/runtime/src/main/resources/META-INF/keycloak.properties similarity index 100% rename from quarkus/server/src/main/resources/META-INF/keycloak.properties rename to quarkus/runtime/src/main/resources/META-INF/keycloak.properties diff --git a/quarkus/runtime/src/main/resources/application.properties b/quarkus/runtime/src/main/resources/application.properties new file mode 100644 index 0000000000..95b6228f21 --- /dev/null +++ b/quarkus/runtime/src/main/resources/application.properties @@ -0,0 +1,26 @@ +# This is the main configuration for Keycloak on Quarkus + +quarkus.package.main-class=keycloak +quarkus.http.root-path=/ +quarkus.application.name=Keycloak +quarkus.banner.enabled=false + +# Disable health checks from extensions, since we provide our own (default is true) +quarkus.health.extensions.enabled=false + +# Default transaction timeout +quarkus.transaction-manager.default-transaction-timeout=300 + +# The JAX-RS application is programmatically registered at build time. +# When indexing classes, both KeycloakApplication and QuarkusKeycloakApplication are indexed and multuple +# application classes are no longer supported by resteasy extension +quarkus.resteasy.ignore-application-classes=true + +# Ignore split packages for Keycloak related packages +quarkus.arc.ignored-split-packages=org.keycloak.* + +# No need to generate dependencies list +quarkus.package.include-dependency-list=false + +# we do not want running dev services in distribution +quarkus.devservices.enabled=false diff --git a/quarkus/server/src/main/resources/application.properties b/quarkus/server/src/main/resources/application.properties index 9dd1924f31..a679430a97 100644 --- a/quarkus/server/src/main/resources/application.properties +++ b/quarkus/server/src/main/resources/application.properties @@ -1,30 +1,7 @@ -#quarkus.log.level = DEBUG +# Inherit all configuration from the default runtime settings and sets those specific to the distribution + quarkus.package.output-name=keycloak quarkus.package.type=mutable-jar quarkus.package.output-directory=lib quarkus.package.user-providers-directory=../providers -quarkus.package.main-class=keycloak - -quarkus.http.root-path=/ -quarkus.application.name=Keycloak -quarkus.banner.enabled=false - -# Disable health checks from extensions, since we provide our own (default is true) -quarkus.health.extensions.enabled=false - -# Default transaction timeout -quarkus.transaction-manager.default-transaction-timeout=300 - -# The JAX-RS application is programmatically registered at build time. -# When indexing classes, both KeycloakApplication and QuarkusKeycloakApplication are indexed and multuple -# application classes are no longer supported by resteasy extension -quarkus.resteasy.ignore-application-classes=true - -# Ignore split packages for Keycloak related packages -quarkus.arc.ignored-split-packages=org.keycloak.* - -# No need to generate dependencies list -quarkus.package.include-dependency-list=false - -# we do not want running dev services in distribution -quarkus.devservices.enabled=false +quarkus.package.main-class=keycloak \ No newline at end of file diff --git a/quarkus/tests/integration/pom.xml b/quarkus/tests/integration/pom.xml new file mode 100644 index 0000000000..0fb7984c2a --- /dev/null +++ b/quarkus/tests/integration/pom.xml @@ -0,0 +1,65 @@ + + + + + 4.0.0 + + + keycloak-quarkus-test-parent + org.keycloak + 16.0.0-SNAPSHOT + ../pom.xml + + + Keycloak Quarkus Server Integration tests + keycloak-quarkus-integration-tests + jar + + + + org.keycloak + keycloak-quarkus-server + + + + + org.keycloak + keycloak-quarkus-server-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-junit5 + + + io.rest-assured + rest-assured + test + + + diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLIResult.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLIResult.java new file mode 100644 index 0000000000..37af70a127 --- /dev/null +++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLIResult.java @@ -0,0 +1,107 @@ +/* + * 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.junit5.extension; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.List; + +import org.keycloak.quarkus.runtime.cli.Picocli; + +import io.quarkus.test.junit.main.LaunchResult; +import picocli.CommandLine; + +public interface CLIResult extends LaunchResult { + + static Object create(List outputStream, List errStream, int exitCode, boolean distribution) { + return new CLIResult() { + @Override + public List getOutputStream() { + return outputStream; + } + + @Override + public List getErrorStream() { + return errStream; + } + + @Override + public int exitCode() { + return exitCode; + } + + @Override + public boolean isDistribution() { + return distribution; + } + }; + } + + boolean isDistribution(); + + default void assertStarted() { + assertTrue(getOutput().contains("Listening on:")); + assertNotDevMode(); + } + + default void assertNotDevMode() { + assertFalse(getOutput().contains("Running the server in dev mode.")); + } + + default void assertStartedDevMode() { + assertTrue(getOutput().contains("Running the server in dev mode.")); + } + + default void assertError(String msg) { + assertTrue(getErrorOutput().contains(msg)); + } + + default void assertHelp(String command) { + if (command == null) { + fail("No command provided"); + } + + CommandLine cmd = Picocli.createCommandLine(Arrays.asList(command, "--help")); + + if (isDistribution()) { + cmd.setCommandName("kc.sh"); + } + + try ( + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(outStream, true) + ) { + if ("kc.sh".equals(command)) { + cmd.usage(printStream); + } else { + cmd.getSubcommands().get(command).usage(printStream); + } + + // not very reliable, we should be comparing the output with some static reference to the help message. + assertTrue(getOutput().equals(outStream.toString().trim())); + } catch (IOException cause) { + throw new RuntimeException("Failed to assert help", cause); + } + } +} diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITest.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITest.java new file mode 100644 index 0000000000..cfb5d59ad8 --- /dev/null +++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITest.java @@ -0,0 +1,30 @@ +/* + * 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.junit5.extension; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ExtendWith; + +@Target(ElementType.TYPE) +@ExtendWith({ CLITestExtension.class }) +@Retention(RetentionPolicy.RUNTIME) +public @interface CLITest { +} diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java new file mode 100644 index 0000000000..22e60739db --- /dev/null +++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java @@ -0,0 +1,172 @@ +/* + * 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.junit5.extension; + +import static org.keycloak.it.junit5.extension.DistributionTest.ReInstall.BEFORE_ALL; +import static org.keycloak.quarkus.runtime.Environment.forceTestLaunchMode; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.keycloak.it.junit5.extension.DistributionTest.ReInstall; +import org.keycloak.it.utils.KeycloakDistribution; +import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.quarkus.runtime.cli.command.Start; +import org.keycloak.quarkus.runtime.cli.command.StartDev; + +import io.quarkus.test.junit.QuarkusMainTestExtension; +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; + +public class CLITestExtension extends QuarkusMainTestExtension { + + private KeycloakDistribution dist; + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + DistributionTest distConfig = getDistributionConfig(context); + + if (distConfig != null) { + Launch launch = context.getRequiredTestMethod().getAnnotation(Launch.class); + + if (launch != null) { + if (dist == null) { + dist = createDistribution(distConfig); + } + dist.start(Arrays.asList(launch.value())); + } + } else { + configureProfile(context); + super.beforeEach(context); + } + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + DistributionTest distConfig = getDistributionConfig(context); + + if (distConfig != null) { + if (distConfig.keepAlive()) { + dist.stopIfRunning(); + } + } + + super.afterEach(context); + } + + @Override + public void afterAll(ExtensionContext context) throws Exception { + if (dist != null) { + // just to make sure the server is stopped after all tests + dist.stopIfRunning(); + } + super.afterAll(context); + } + + private KeycloakDistribution createDistribution(DistributionTest config) { + KeycloakDistribution distribution = new KeycloakDistribution(); + + distribution.setReCreate(!ReInstall.NEVER.equals(config.reInstall())); + distribution.setDebug(config.debug()); + distribution.setManualStop(config.keepAlive()); + + return distribution; + } + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + DistributionTest distConfig = getDistributionConfig(context); + + if (distConfig != null) { + if (BEFORE_ALL.equals(distConfig.reInstall())) { + dist = createDistribution(distConfig); + } + } else { + forceTestLaunchMode(); + } + + super.beforeAll(context); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext context) + throws ParameterResolutionException { + Class type = parameterContext.getParameter().getType(); + + if (type == LaunchResult.class) { + List outputStream; + List errStream; + int exitCode; + + boolean isDistribution = getDistributionConfig(context) != null; + + if (isDistribution) { + outputStream = dist.getOutputStream(); + errStream = dist.getErrorStream(); + exitCode = dist.getExitCode(); + } else { + LaunchResult result = (LaunchResult) super.resolveParameter(parameterContext, context); + outputStream = result.getOutputStream(); + errStream = result.getErrorStream(); + exitCode = result.exitCode(); + } + + return CLIResult.create(outputStream, errStream, exitCode, isDistribution); + } + + // for now, not support for manual launching using QuarkusMainLauncher + throw new RuntimeException("Parameter type [" + type + "] not supported"); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + Class type = parameterContext.getParameter().getType(); + return type == LaunchResult.class; + } + + private void configureProfile(ExtensionContext context) { + List cliArgs = getCliArgs(context); + + // when running tests, build steps happen before executing our CLI so that profiles are not set and not taken + // into account when executing the build steps + // this is basically reproducing the behavior when using kc.sh + if (cliArgs.contains(Start.NAME)) { + Environment.setProfile("prod"); + } else if (cliArgs.contains(StartDev.NAME)) { + Environment.forceDevProfile(); + } + } + + private List getCliArgs(ExtensionContext context) { + Launch annotation = context.getRequiredTestMethod().getAnnotation(Launch.class); + + if (annotation != null) { + return Arrays.asList(annotation.value()); + } + + return Collections.emptyList(); + } + + private DistributionTest getDistributionConfig(ExtensionContext context) { + return context.getTestClass().get().getDeclaredAnnotation(DistributionTest.class); + } +} diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/DistributionTest.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/DistributionTest.java new file mode 100644 index 0000000000..1aa86f008b --- /dev/null +++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/DistributionTest.java @@ -0,0 +1,42 @@ +/* + * 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.junit5.extension; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ExtendWith; + +@Target(ElementType.TYPE) +@ExtendWith({ CLITestExtension.class }) +@Retention(RetentionPolicy.RUNTIME) +public @interface DistributionTest { + + boolean debug() default false; + boolean keepAlive() default false; + + enum ReInstall { + BEFORE_ALL, + BEFORE_TEST, + NEVER; + } + + ReInstall reInstall() default ReInstall.BEFORE_ALL; +} + diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/KeycloakDistribution.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/KeycloakDistribution.java new file mode 100644 index 0000000000..fa393fb118 --- /dev/null +++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/KeycloakDistribution.java @@ -0,0 +1,337 @@ +/* + * 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.utils; + +import static org.keycloak.quarkus.runtime.Environment.LAUNCH_MODE; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.apache.commons.io.FileUtils; +import org.eclipse.aether.artifact.Artifact; +import org.jboss.logging.Logger; + +import io.quarkus.bootstrap.util.ZipUtils; + +public final class KeycloakDistribution { + + private static final Logger LOGGER = Logger.getLogger(KeycloakDistribution.class); + + private Process keycloak; + private int exitCode = -1; + private final Path distPath; + private final List outputStream = new ArrayList<>(); + private final List errorStream = new ArrayList<>(); + private boolean reCreate; + private boolean manualStop; + private String relativePath; + private int httpPort; + private boolean debug; + private ExecutorService outputExecutor; + + public KeycloakDistribution() { + distPath = prepareDistribution(); + } + + public void start(List arguments) { + reset(); + if (manualStop && isRunning()) { + throw new IllegalStateException("Server already running. You should manually stop the server before starting it again."); + } + stopIfRunning(); + try { + startServer(arguments); + if (manualStop) { + asyncReadOutput(); + waitForReadiness(); + } else { + readOutput(); + } + } catch (Exception cause) { + stopIfRunning(); + throw new RuntimeException("Failed to start the server", cause); + } finally { + if (!manualStop) { + stopIfRunning(); + } + } + } + + public void stopIfRunning() { + if (isRunning()) { + try { + keycloak.destroy(); + keycloak.waitFor(10, TimeUnit.SECONDS); + exitCode = keycloak.exitValue(); + } catch (Exception cause) { + keycloak.destroyForcibly(); + throw new RuntimeException("Failed to stop the server", cause); + } + } + + shutdownOutputExecutor(); + } + + public List getOutputStream() { + return outputStream; + } + + public List getErrorStream() { + return errorStream; + } + + public int getExitCode() { + return exitCode; + } + + public void setReCreate(boolean reCreate) { + this.reCreate = reCreate; + } + + public void setDebug(boolean debug) { + this.debug = debug; + } + + public void setManualStop(boolean manualStop) { + this.manualStop = manualStop; + } + + private String[] getCliArgs(List arguments) { + List commands = new ArrayList<>(); + + commands.add("./kc.sh"); + + if (debug) { + commands.add("--debug"); + } + + if (!manualStop) { + commands.add("-D" + LAUNCH_MODE + "=test"); + } + + this.relativePath = arguments.stream().filter(arg -> arg.startsWith("--http-relative-path")).map(arg -> arg.substring(arg.indexOf('=') + 1)).findAny().orElse("/"); + this.httpPort = Integer.parseInt(arguments.stream().filter(arg -> arg.startsWith("--http-port")).map(arg -> arg.substring(arg.indexOf('=') + 1)).findAny().orElse("8080")); + + commands.addAll(arguments); + + return commands.toArray(new String[0]); + } + + private void waitForReadiness() throws MalformedURLException { + URL contextRoot = new URL("http://localhost:" + httpPort + ("/" + relativePath + "/realms/master/").replace("//", "/")); + HttpURLConnection connection = null; + long startTime = System.currentTimeMillis(); + + while (true) { + if (System.currentTimeMillis() - startTime > getStartTimeout()) { + throw new IllegalStateException( + "Timeout [" + getStartTimeout() + "] while waiting for Quarkus server"); + } + + try { + // wait before checking for opening a new connection + Thread.sleep(1000); + if ("https".equals(contextRoot.getProtocol())) { + HttpsURLConnection httpsConnection = (HttpsURLConnection) (connection = (HttpURLConnection) contextRoot.openConnection()); + httpsConnection.setSSLSocketFactory(createInsecureSslSocketFactory()); + httpsConnection.setHostnameVerifier(createInsecureHostnameVerifier()); + } else { + connection = (HttpURLConnection) contextRoot.openConnection(); + } + + connection.setReadTimeout((int) getStartTimeout()); + connection.setConnectTimeout((int) getStartTimeout()); + connection.connect(); + + if (connection.getResponseCode() == 200) { + LOGGER.infof("Keycloak is ready at %s", contextRoot); + break; + } + } catch (Exception ignore) { + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + } + + private long getStartTimeout() { + return TimeUnit.SECONDS.toMillis(120); + } + + private HostnameVerifier createInsecureHostnameVerifier() { + return new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }; + } + + private SSLSocketFactory createInsecureSslSocketFactory() throws IOException { + TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() { + public void checkClientTrusted(final X509Certificate[] chain, final String authType) { + } + + public void checkServerTrusted(final X509Certificate[] chain, final String authType) { + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }}; + + SSLContext sslContext; + SSLSocketFactory socketFactory; + + try { + sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAllCerts, new SecureRandom()); + socketFactory = sslContext.getSocketFactory(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new IOException("Can't create unsecure trust manager"); + } + return socketFactory; + } + + private boolean isRunning() { + return keycloak != null && keycloak.isAlive(); + } + + private void asyncReadOutput() { + shutdownOutputExecutor(); + outputExecutor = Executors.newSingleThreadExecutor(); + outputExecutor.execute(this::readOutput); + } + + private void shutdownOutputExecutor() { + if (outputExecutor != null) { + outputExecutor.shutdown(); + try { + outputExecutor.awaitTermination(30, TimeUnit.SECONDS); + } catch (InterruptedException cause) { + throw new RuntimeException("Failed to terminate output executor", cause); + } finally { + outputExecutor = null; + } + } + } + + private void reset() { + outputStream.clear(); + errorStream.clear(); + exitCode = -1; + keycloak = null; + shutdownOutputExecutor(); + } + + private Path prepareDistribution() { + try { + Path distRootPath = Paths.get(System.getProperty("java.io.tmpdir")).resolve("kc-tests"); + distRootPath.toFile().mkdirs(); + File distFile = Maven.resolveArtifact("org.keycloak", "keycloak-server-x-dist", "zip") + .map(Artifact::getFile) + .orElseThrow(new Supplier() { + @Override + public RuntimeException get() { + return new RuntimeException("Could not obtain distribution artifact"); + } + }); + String distDirName = distFile.getName().replace("keycloak-server-x-dist", "keycloak.x"); + Path distPath = distRootPath.resolve(distDirName.substring(0, distDirName.lastIndexOf('.'))); + + if (reCreate || !distPath.toFile().exists()) { + distPath.toFile().delete(); + ZipUtils.unzip(distFile.toPath(), distRootPath); + } + + // make sure kc.sh is executable + distPath.resolve("bin").resolve("kc.sh").toFile().setExecutable(true); + + return distPath; + } catch (Exception cause) { + throw new RuntimeException("Failed to prepare distribution", cause); + } + } + + private void readOutput() { + try ( + BufferedReader outStream = new BufferedReader(new InputStreamReader(keycloak.getInputStream())); + BufferedReader errStream = new BufferedReader(new InputStreamReader(keycloak.getErrorStream())); + ) { + while (keycloak.isAlive()) { + readStream(outStream, outputStream); + readStream(errStream, errorStream); + } + } catch (Throwable cause) { + throw new RuntimeException("Failed to read server output", cause); + } + } + + private void readStream(BufferedReader reader, List stream) throws IOException { + String line; + + while (reader.ready() && (line = reader.readLine()) != null) { + stream.add(line); + System.out.println(line); + } + } + + /** + * The server is configured to redirect errors to output stream. This adds a limitation when checking whether a + * message arrived via error stream. + * + * @param arguments the list of arguments to run the server + * @throws Exception if something bad happens + */ + private void startServer(List arguments) throws Exception { + ProcessBuilder pb = new ProcessBuilder(getCliArgs(arguments)); + ProcessBuilder builder = pb.directory(distPath.resolve("bin").toFile()); + + builder.environment().put("KEYCLOAK_ADMIN", "admin"); + builder.environment().put("KEYCLOAK_ADMIN_PASSWORD", "admin"); + + FileUtils.deleteDirectory(distPath.resolve("data").toFile()); + + keycloak = builder.start(); + } +} diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/Maven.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/Maven.java new file mode 100644 index 0000000000..256dde7129 --- /dev/null +++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/Maven.java @@ -0,0 +1,80 @@ +/* + * 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.utils; + +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.resolution.ArtifactResult; + +import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; +import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; +import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; +import io.quarkus.bootstrap.utils.BuildToolHelper; + +public final class Maven { + + private static Path resolveProjectDir() { + try { + String classFilePath = KeycloakDistribution.class.getName().replace(".", "/") + ".class"; + URL classFileResource = Thread.currentThread().getContextClassLoader().getResource(classFilePath); + String classPath = classFileResource.getPath(); + classPath = classPath.substring(0, classPath.length() - classFilePath.length()); + URL newResource = new URL(classFileResource.getProtocol(), classFileResource.getHost(), classFileResource.getPort(), + classPath); + + return BuildToolHelper.getProjectDir(Paths.get(newResource.toURI())); + } catch (Exception cause) { + throw new RuntimeException("Failed to resolve project dir", cause); + } + } + + static Optional resolveArtifact(String groupId, String artifactId) { + return resolveArtifact(groupId, artifactId, "jar"); + } + + static Optional resolveArtifact(String groupId, String artifactId, String extension) { + BootstrapMavenContext mvnCtx = createBootstrapContext(); + LocalProject project = mvnCtx.getCurrentProject(); + + try { + MavenArtifactResolver mvnResolver = new MavenArtifactResolver(mvnCtx); + ArtifactResult resolve = mvnResolver.resolve(new DefaultArtifact(groupId, artifactId, extension, + project.getVersion())); + + if (resolve.isResolved()) { + return Optional.of(resolve.getArtifact()); + } + } catch (Exception cause) { + throw new RuntimeException("Failed to resolve project artifact [" + groupId + ":" + artifactId + ":" + project.getVersion() + ":" + extension, cause); + } + + return Optional.empty(); + } + + private static BootstrapMavenContext createBootstrapContext() { + try { + return new BootstrapMavenContext(BootstrapMavenContext.config().setCurrentProject(resolveProjectDir().toString())); + } catch (Exception cause) { + throw new RuntimeException("Failed to create maven boorstrap context", cause); + } + } +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/HelpCommandTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/HelpCommandTest.java new file mode 100644 index 0000000000..ef5e48d0ea --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/HelpCommandTest.java @@ -0,0 +1,59 @@ +/* + * 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.Assertions; +import org.junit.jupiter.api.Test; +import org.keycloak.it.junit5.extension.CLIResult; +import org.keycloak.it.junit5.extension.CLITest; +import org.keycloak.quarkus.runtime.cli.command.Main; + +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; + +@CLITest +public class HelpCommandTest { + + @Test + @Launch({ "--help" }) + void testHelpCommand(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertHelp("kc.sh"); + } + + @Test + @Launch({ "start", "--help" }) + void testStartHelpCommand(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertHelp("start"); + } + + @Test + @Launch({ "start-dev", "--help" }) + void testStartDevCommand(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertHelp("start-dev"); + } + + @Test + @Launch({ "build", "--help" }) + void testBuildCommand(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertHelp("build"); + } +} 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 new file mode 100644 index 0000000000..0076af2e37 --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/OptionValidationTest.java @@ -0,0 +1,49 @@ +/* + * 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.Assertions; +import org.junit.jupiter.api.Test; +import org.keycloak.it.junit5.extension.CLIResult; +import org.keycloak.it.junit5.extension.CLITest; + +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; +import io.quarkus.test.junit.main.QuarkusMainLauncher; + +@CLITest +public class OptionValidationTest { + + @Test + @Launch({"build", "--db"}) + public void failMissingOptionValue(LaunchResult result) { + Assertions.assertTrue(result.getErrorOutput().contains("Missing required value for option '--db' (vendor). Expected values are: h2-file, h2-mem, mariadb, mssql, mssql-2012, mysql, oracle, postgres, postgres-95")); + } + + @Test + @Launch({"build", "--db=invalid"}) + public void failInvalidOptionValue(LaunchResult result) { + Assertions.assertTrue(result.getErrorOutput().contains("Invalid value for option '--db': invalid. Expected values are: h2-file, h2-mem, mariadb, mssql, mssql-2012, mysql, oracle, postgres, postgres-95")); + } + + @Test + @Launch({"build", "--db", "foo", "bar"}) + public void failMultipleOptionValue(LaunchResult result) { + Assertions.assertTrue(result.getErrorOutput().contains("Option '--db' expects a single value (vendor) Expected values are: h2-file, h2-mem, mariadb, mssql, mssql-2012, mysql, oracle, postgres, postgres-95")); + } +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/ShowConfigCommandTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/ShowConfigCommandTest.java new file mode 100644 index 0000000000..6bf92b353e --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/ShowConfigCommandTest.java @@ -0,0 +1,47 @@ +/* + * 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.Assertions; +import org.junit.jupiter.api.Test; +import org.keycloak.it.junit5.extension.CLITest; + +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; + +@CLITest +class ShowConfigCommandTest { + + @Test + @Launch({ "show-config" }) + void testShowConfigCommandShowsRuntimeConfig(LaunchResult result) { + Assertions.assertTrue(result.getOutput() + .contains("Runtime Configuration")); + } + + @Test + @Launch({ "show-config", "all" }) + void testShowConfigCommandWithAllShowsAllProfiles(LaunchResult result) { + Assertions.assertTrue(result.getOutput() + .contains("Runtime Configuration")); + Assertions.assertTrue(result.getOutput() + .contains("Quarkus Configuration")); + Assertions.assertTrue(result.getOutput() + .contains("Profile \"import_export\" Configuration")); + } +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/StartCommandTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/StartCommandTest.java new file mode 100644 index 0000000000..0ef2476d56 --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/StartCommandTest.java @@ -0,0 +1,63 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.keycloak.it.junit5.extension.CLIResult; +import org.keycloak.it.junit5.extension.CLITest; + +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; + +@CLITest +public class StartCommandTest { + + @Test + @Launch({ "start", "--hostname-strict=false" }) + void failNoTls(LaunchResult result) { + assertTrue(result.getOutput().contains("Key material not provided to setup HTTPS")); + } + + @Test + @Launch({ "start", "--http-enabled=true" }) + void failNoHostnameNotSet(LaunchResult result) { + assertTrue(result.getOutput().contains("ERROR: Strict hostname resolution configured but no hostname was set")); + } + + @Test + @Launch({ "--profile=dev", "start" }) + void failUsingDevProfile(LaunchResult result) { + assertTrue(result.getErrorOutput().contains("ERROR: You can not 'start' the server using the 'dev' configuration profile. Please re-build the server first, using 'kc.sh build' for the default production profile, or using 'kc.sh build --profile=' with a profile more suitable for production.")); + } + + @Test + @Launch({ "-v", "start", "--http-enabled=true", "--hostname-strict=false" }) + void testHttpEnabled(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertStarted(); + } + + @Test + @Launch({ "-v", "start", "--db=h2-mem" }) + void failBuildPropertyNotAvailable(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertError("Unknown option: '--db=h2-mem'"); + } +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/StartDevCommandTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/StartDevCommandTest.java new file mode 100644 index 0000000000..b2c0042117 --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/StartDevCommandTest.java @@ -0,0 +1,43 @@ +/* + * 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 io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; + +@CLITest +public class StartDevCommandTest { + + @Test + @Launch({ "start-dev" }) + void testDevModeWarning(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertStartedDevMode(); + } + + @Test + @Launch({ "start-dev", "--db=h2-mem" }) + void testBuildPropertyAvailable(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertStartedDevMode(); + } +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildCommandDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildCommandDistTest.java new file mode 100644 index 0000000000..649ddd6221 --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildCommandDistTest.java @@ -0,0 +1,47 @@ +/* + * 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.dist; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.keycloak.it.junit5.extension.DistributionTest; + +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; + +@DistributionTest +class BuildCommandDistTest { + + @Test + @Launch({ "build" }) + void resetConfig(LaunchResult result) { + assertTrue(result.getOutput().contains("Updating the configuration and installing your custom providers, if any. Please wait.")); + assertTrue(result.getOutput().contains("Quarkus augmentation completed")); + assertTrue(result.getOutput().contains("Server configuration updated and persisted. Run the following command to review the configuration:")); + assertTrue(result.getOutput().contains("kc.sh show-config")); + } + + @Test + @Launch({ "--profile=dev", "build" }) + void failIfDevProfile(LaunchResult result) { + assertTrue(result.getErrorOutput().contains("ERROR: Failed to run 'build' command.")); + assertTrue(result.getErrorOutput().contains("ERROR: You can not 'build' the server using the 'dev' configuration profile. Please re-build the server first, using 'kc.sh build' for the default production profile, or using 'kc.sh build --profile=' with a profile more suitable for production.")); + assertTrue(result.getErrorOutput().contains("For more details run the same command passing the '--verbose' option. Also you can use '--help' to see the details about the usage of the particular command.")); + } +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HelpCommandDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HelpCommandDistTest.java new file mode 100644 index 0000000000..5e4feaf75c --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HelpCommandDistTest.java @@ -0,0 +1,25 @@ +/* + * 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.dist; + +import org.keycloak.it.cli.HelpCommandTest; +import org.keycloak.it.junit5.extension.DistributionTest; + +@DistributionTest +public class HelpCommandDistTest extends HelpCommandTest { +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/MetricsDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/MetricsDistTest.java new file mode 100644 index 0000000000..4bc71aca55 --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/MetricsDistTest.java @@ -0,0 +1,48 @@ +/* + * 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.dist; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.containsString; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.keycloak.it.junit5.extension.DistributionTest; + +import io.quarkus.test.junit.main.Launch; + +@DistributionTest(keepAlive =true) +public class MetricsDistTest { + + @Test + @Launch({ "start-dev", "--metrics-enabled=true" }) + void testMetricsEndpoint() { + when().get("/metrics").then() + .statusCode(200) + .body(containsString("base_gc_total")); + } + + @Disabled("https://github.com/keycloak/keycloak/pull/8878") + @Test + @Launch({ "start-dev", "--http-relative-path=/auth", "--metrics-enabled=true" }) + void testMetricsEndpointUsingRelativePath() { + when().get("/auth/metrics").then() + .statusCode(200) + .body(containsString("base_gc_total")); + } +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java new file mode 100644 index 0000000000..580ee30606 --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java @@ -0,0 +1,25 @@ +/* + * 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.dist; + +import org.keycloak.it.cli.StartCommandTest; +import org.keycloak.it.junit5.extension.DistributionTest; + +@DistributionTest +public class StartCommandDistTest extends StartCommandTest { +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartDevCommandDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartDevCommandDistTest.java new file mode 100644 index 0000000000..44cf85a646 --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartDevCommandDistTest.java @@ -0,0 +1,25 @@ +/* + * 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.dist; + +import org.keycloak.it.cli.StartDevCommandTest; +import org.keycloak.it.junit5.extension.DistributionTest; + +@DistributionTest +public class StartDevCommandDistTest extends StartDevCommandTest { +} diff --git a/quarkus/tests/pom.xml b/quarkus/tests/pom.xml new file mode 100644 index 0000000000..ef0fe07c99 --- /dev/null +++ b/quarkus/tests/pom.xml @@ -0,0 +1,39 @@ + + + + + 4.0.0 + + + keycloak-quarkus-parent + org.keycloak + 16.0.0-SNAPSHOT + ../pom.xml + + + Keycloak Quarkus Test Parent + keycloak-quarkus-test-parent + pom + + + integration + + +