diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d39a56469b3..a1c600ac149 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,11 @@ on: [push, pull_request] env: DEFAULT_JDK_VERSION: 11 +concurrency: + # Only run once for latest commit per ref and cancel other (previous) runs. + group: ci-keycloak-${{ github.ref }} + cancel-in-progress: true + jobs: build: name: Build @@ -76,7 +81,7 @@ jobs: name: keycloak-artifacts.zip - name: Run unit tests run: | - if ! mvn install -nsu -B -DskipTestsuite -DskipExamples -f pom.xml; then + if ! mvn install -nsu -B -DskipTestsuite -DskipQuarkus -DskipExamples -f pom.xml; then find . -path '*/target/surefire-reports/*.xml' | zip -q reports-unit-tests.zip -@ exit 1 fi @@ -403,9 +408,13 @@ jobs: java-version: ${{ env.DEFAULT_JDK_VERSION }} - name: Update maven settings run: mkdir -p ~/.m2 ; cp .github/settings.xml ~/.m2/ - - name: Run Quarkus Tests + + - name: Prepare the local distribution archives + run: mvn clean install -DskipTests -Pdistribution + + - name: Run Quarkus Integration Tests run: | - mvn clean install -nsu -B -f quarkus/tests/pom.xml | misc/log/trimmer.sh + 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 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 index 37af70a1279..8dac68cb642 100644 --- 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 @@ -61,20 +61,23 @@ public interface CLIResult extends LaunchResult { boolean isDistribution(); default void assertStarted() { - assertTrue(getOutput().contains("Listening on:")); + assertTrue(getOutput().contains("Listening on:"), () -> "The standard output:\n" + getOutput() + "does include \"Listening on:\""); assertNotDevMode(); } default void assertNotDevMode() { - assertFalse(getOutput().contains("Running the server in dev mode.")); + assertFalse(getOutput().contains("Running the server in dev mode."), + () -> "The standard output:\n" + getOutput() + "does include the Start Dev output"); } default void assertStartedDevMode() { - assertTrue(getOutput().contains("Running the server in dev mode.")); + assertTrue(getOutput().contains("Running the server in dev mode."), + () -> "The standard output:\n" + getOutput() + "doesn't include the Start Dev output"); } default void assertError(String msg) { - assertTrue(getErrorOutput().contains(msg)); + assertTrue(getErrorOutput().contains(msg), + () -> "The Error Output:\n " + getErrorOutput() + "\ndoesn't contains " + msg); } default void assertHelp(String command) { @@ -99,7 +102,8 @@ public interface CLIResult extends LaunchResult { } // not very reliable, we should be comparing the output with some static reference to the help message. - assertTrue(getOutput().equals(outStream.toString().trim())); + assertTrue(getOutput().trim().equals(outStream.toString().trim()), + () -> "The Output:\n " + getOutput() + "\ndoesnt't contains " + 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/CLITestExtension.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java index 22e60739dbd..6f34b560d40 100644 --- 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 @@ -26,8 +26,8 @@ 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.it.utils.RawKeycloakDistribution; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.cli.command.Start; import org.keycloak.quarkus.runtime.cli.command.StartDev; @@ -65,7 +65,7 @@ public class CLITestExtension extends QuarkusMainTestExtension { if (distConfig != null) { if (distConfig.keepAlive()) { - dist.stopIfRunning(); + dist.stop(); } } @@ -76,17 +76,17 @@ public class CLITestExtension extends QuarkusMainTestExtension { public void afterAll(ExtensionContext context) throws Exception { if (dist != null) { // just to make sure the server is stopped after all tests - dist.stopIfRunning(); + dist.stop(); } 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()); + KeycloakDistribution distribution = new RawKeycloakDistribution( + config.debug(), + config.keepAlive(), + !DistributionTest.ReInstall.NEVER.equals(config.reInstall()) + ); return distribution; } 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 index fa393fb1188..452e072f5e7 100644 --- 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 @@ -1,337 +1,41 @@ -/* - * 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.util.ArrayList; +import java.util.List; + 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; +public interface KeycloakDistribution { -import io.quarkus.bootstrap.util.ZipUtils; + void start(List arguments); -public final class KeycloakDistribution { + void stop(); - private static final Logger LOGGER = Logger.getLogger(KeycloakDistribution.class); + List getOutputStream(); - 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; + List getErrorStream(); - public KeycloakDistribution() { - distPath = prepareDistribution(); - } + int getExitCode(); - 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(); - } - } - } + boolean isDebug(); - 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); - } - } + boolean isManualStop(); - 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) { + default String[] getCliArgs(List arguments) { List commands = new ArrayList<>(); commands.add("./kc.sh"); - if (debug) { + if (this.isDebug()) { commands.add("--debug"); } - if (!manualStop) { + if (!this.isManualStop()) { 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 deleted file mode 100644 index 256dde7129a..00000000000 --- a/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/Maven.java +++ /dev/null @@ -1,80 +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.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/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java new file mode 100644 index 00000000000..c67f8ba1331 --- /dev/null +++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java @@ -0,0 +1,318 @@ +/* + * 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.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 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.jboss.logging.Logger; + +import io.quarkus.bootstrap.util.ZipUtils; +import org.keycloak.common.Version; + +public final class RawKeycloakDistribution implements KeycloakDistribution { + + private static final Logger LOGGER = Logger.getLogger(RawKeycloakDistribution.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 manualStop; + private String relativePath; + private int httpPort; + private boolean debug; + private boolean reCreate; + private ExecutorService outputExecutor; + + public RawKeycloakDistribution(boolean debug, boolean manualStop, boolean reCreate) { + this.debug = debug; + this.manualStop = manualStop; + this.reCreate = reCreate; + this.distPath = prepareDistribution(); + } + + @Override + 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."); + } + stop(); + try { + startServer(arguments); + if (manualStop) { + asyncReadOutput(); + waitForReadiness(); + } else { + readOutput(); + } + } catch (Exception cause) { + stop(); + throw new RuntimeException("Failed to start the server", cause); + } finally { + if (!manualStop) { + stop(); + } + } + } + + @Override + public void stop() { + 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(); + } + + @Override + public List getOutputStream() { + return outputStream; + } + @Override + public List getErrorStream() { + return errorStream; + } + @Override + public int getExitCode() { + return exitCode; + } + @Override + public boolean isDebug() { return this.debug; } + @Override + public boolean isManualStop() { return this.manualStop; } + + @Override + public String[] getCliArgs(List arguments) { + 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")); + + return KeycloakDistribution.super.getCliArgs(arguments); + } + + 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 = new File("../../../distribution/server-x-dist/target/keycloak.x-" + Version.VERSION_KEYCLOAK + ".zip"); + if (!distFile.exists()) { + throw new RuntimeException("Distribution archive " + distFile.getAbsolutePath() +" doesn't exists"); + } + distRootPath.toFile().mkdirs(); + 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 + if (!distPath.resolve("bin").resolve("kc.sh").toFile().setExecutable(true)) { + throw new RuntimeException("Cannot set kc.sh executable"); + } + + 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/test/java/org/keycloak/it/cli/StartCommandTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/StartCommandTest.java index 0ef2476d564..02f2361b99a 100644 --- 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 @@ -32,19 +32,22 @@ public class StartCommandTest { @Test @Launch({ "start", "--hostname-strict=false" }) void failNoTls(LaunchResult result) { - assertTrue(result.getOutput().contains("Key material not provided to setup HTTPS")); + assertTrue(result.getOutput().contains("Key material not provided to setup HTTPS"), + () -> "The Output:\n" + result.getOutput() + "doesn't contains the expected string."); } @Test @Launch({ "start", "--http-enabled=true" }) void failNoHostnameNotSet(LaunchResult result) { - assertTrue(result.getOutput().contains("ERROR: Strict hostname resolution configured but no hostname was set")); + assertTrue(result.getOutput().contains("ERROR: Strict hostname resolution configured but no hostname was set"), + () -> "The Output:\n" + result.getOutput() + "doesn't contains the expected string."); } @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.")); + 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."), + () -> "The Output:\n" + result.getErrorOutput() + "doesn't contains the expected string."); } @Test 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 index 649ddd6221c..8c76de5c1ba 100644 --- 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 @@ -31,17 +31,24 @@ 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")); + assertTrue(result.getOutput().contains("Updating the configuration and installing your custom providers, if any. Please wait."), + () -> "The Output:\n" + result.getOutput() + "doesn't contains the expected string."); + assertTrue(result.getOutput().contains("Quarkus augmentation completed"), + () -> "The Output:\n" + result.getOutput() + "doesn't contains the expected string."); + assertTrue(result.getOutput().contains("Server configuration updated and persisted. Run the following command to review the configuration:"), + () -> "The Output:\n" + result.getOutput() + "doesn't contains the expected string."); + assertTrue(result.getOutput().contains("kc.sh show-config"), + () -> "The Output:\n" + result.getOutput() + "doesn't contains the expected string."); } @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.")); + assertTrue(result.getErrorOutput().contains("ERROR: Failed to run 'build' command."), + () -> "The Error Output:\n" + result.getErrorOutput() + "doesn't contains the expected string."); + 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."), + () -> "The Error Output:\n" + result.getErrorOutput() + "doesn't contains the expected string."); + 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."), + () -> "The Error Output:\n" + result.getErrorOutput() + "doesn't contains the expected string."); } } diff --git a/quarkus/tests/pom.xml b/quarkus/tests/pom.xml index ef0fe07c992..fe4c768991d 100644 --- a/quarkus/tests/pom.xml +++ b/quarkus/tests/pom.xml @@ -32,8 +32,18 @@ keycloak-quarkus-test-parent pom - - integration - + + + noIntegrations + + + !skipQuarkus + + + + integration + + +