diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java index 6727d13096..0ffd6d8c09 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java @@ -18,6 +18,8 @@ package org.keycloak.quarkus.deployment; import static org.keycloak.quarkus.runtime.configuration.Configuration.getPropertyNames; +import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS; +import static org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource.QUARKUS_PROPERTY_ENABLED; import static org.keycloak.quarkus.runtime.storage.database.jpa.QuarkusJpaConnectionProviderFactory.QUERY_PROPERTY_PREFIX; import static org.keycloak.connections.jpa.util.JpaUtils.loadSpecificNamedQueries; import static org.keycloak.representations.provider.ScriptProviderDescriptor.AUTHENTICATORS; @@ -82,6 +84,7 @@ import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters; import org.jboss.resteasy.spi.ResteasyDeployment; import org.keycloak.Config; import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; +import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication; @@ -313,14 +316,21 @@ class KeycloakProcessor { for (String name : getPropertyNames()) { PropertyMapper mapper = PropertyMappers.getMapper(name); + ConfigValue value = null; if (mapper == null) { - continue; + if (name.startsWith(NS_QUARKUS)) { + value = Configuration.getConfigValue(name); + + if (!QuarkusPropertiesConfigSource.isSameSource(value)) { + continue; + } + } + } else if (mapper.isBuildTime()) { + value = Configuration.getConfigValue(mapper.getFrom()); } - ConfigValue value = Configuration.getConfigValue(mapper.getFrom()); - - if (mapper.isBuildTime() && value != null && value.getValue() != null) { + if (value != null && value.getValue() != null) { properties.put(name, value.getValue()); } } @@ -329,6 +339,8 @@ class KeycloakProcessor { properties.put(String.format("kc.provider.file.%s.last-modified", jar.getName()), String.valueOf(jar.lastModified())); } + properties.put(QUARKUS_PROPERTY_ENABLED, String.valueOf(QuarkusPropertiesConfigSource.getConfigurationFile() != null)); + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { properties.store(outputStream, " Auto-generated, DO NOT change this file"); resources.produce(new GeneratedResourceBuildItem(PersistedConfigSource.PERSISTED_PROPERTIES, outputStream.toByteArray())); 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 7489f7df8a..ba3efe366c 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 @@ -31,6 +31,7 @@ import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; +import io.quarkus.bootstrap.runner.RunnerClassLoader; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ProfileManager; import org.apache.commons.lang3.SystemUtils; @@ -48,7 +49,11 @@ public final class Environment { private Environment() {} public static Boolean isRebuild() { - return Boolean.getBoolean("quarkus.launch.rebuild"); + return !isRuntimeMode(); + } + + public static Boolean isRuntimeMode() { + return Thread.currentThread().getContextClassLoader() instanceof RunnerClassLoader; } public static String getHomeDir() { @@ -187,4 +192,8 @@ public final class Environment { public static boolean isDistribution() { return getHomeDir() != null; } + + public static boolean isRebuildCheck() { + return Boolean.getBoolean("kc.config.rebuild-and-exit"); + } } 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 c281b234b4..37bc691827 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 @@ -78,7 +78,7 @@ public final class Picocli { public static void parseAndRun(List cliArgs) { CommandLine cmd = createCommandLine(cliArgs); - if (Boolean.getBoolean("kc.config.rebuild-and-exit")) { + if (Environment.isRebuildCheck()) { runReAugmentationIfNeeded(cliArgs, cmd); Quarkus.asyncExit(cmd.getCommandSpec().exitCodeOnSuccess()); return; 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 bb17716299..89cd97c738 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 @@ -43,6 +43,9 @@ public class KeycloakConfigSourceProvider implements ConfigSourceProvider { CONFIG_SOURCES.add(new ConfigArgsConfigSource()); CONFIG_SOURCES.add(new KcEnvConfigSource()); + + CONFIG_SOURCES.addAll(new QuarkusPropertiesConfigSource().getConfigSources(Thread.currentThread().getContextClassLoader())); + CONFIG_SOURCES.add(PersistedConfigSource.getInstance()); CONFIG_SOURCES.addAll(new KeycloakPropertiesConfigSource.InFileSystem().getConfigSources(Thread.currentThread().getContextClassLoader())); 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 30107db615..04c954d0ef 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 @@ -28,12 +28,13 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.regex.Pattern; import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.ConfigSourceProvider; import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; +import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; import io.smallrye.config.AbstractLocationConfigSourceLoader; import io.smallrye.config.PropertiesConfigSource; @@ -62,7 +63,7 @@ public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSource @Override protected ConfigSource loadConfigSource(URL url, int ordinal) throws IOException { - return new PropertiesConfigSource(transform(ConfigSourceUtil.urlToMap(url)), KEYCLOAK_CONF_FILE, ordinal); + return new PropertiesConfigSource(transform(ConfigSourceUtil.urlToMap(url)), url.toString(), ordinal); } public static class InClassPath extends KeycloakPropertiesConfigSource implements ConfigSourceProvider { @@ -143,11 +144,22 @@ public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSource Map result = new HashMap<>(properties.size()); properties.keySet().forEach(k -> { String key = transformKey(k); - String value = replaceProperties(properties.get(k)); + PropertyMapper mapper = PropertyMappers.getMapper(key); - result.put(key, value); - result.put(getMappedPropertyName(key), value); + //TODO: remove explicit checks for spi and feature options once we have proper support in our config mappers + if (mapper != null + || key.contains(NS_KEYCLOAK_PREFIX + "spi") + || key.contains(NS_KEYCLOAK_PREFIX + "feature")) { + String value = replaceProperties(properties.get(k)); + + result.put(key, value); + + if (mapper != null && key.charAt(0) != '%') { + result.put(getMappedPropertyName(key), value); + } + } }); + return result; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappingInterceptor.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappingInterceptor.java index d43f7afff7..b7203a129f 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappingInterceptor.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/PropertyMappingInterceptor.java @@ -16,10 +16,14 @@ */ package org.keycloak.quarkus.runtime.configuration; +import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS; + +import io.quarkus.runtime.configuration.AbstractRawDefaultConfigSource; import io.smallrye.config.ConfigSourceInterceptor; import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigValue; import org.keycloak.common.util.StringPropertyReplacer; +import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; @@ -35,6 +39,8 @@ import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; */ public class PropertyMappingInterceptor implements ConfigSourceInterceptor { + private final boolean isQuarkusPropertiesEnabled = QuarkusPropertiesConfigSource.isQuarkusPropertiesEnabled(); + @Override public ConfigValue getValue(ConfigSourceInterceptorContext context, String name) { ConfigValue value = PropertyMappers.getValue(context, name); @@ -43,6 +49,11 @@ public class PropertyMappingInterceptor implements ConfigSourceInterceptor { return null; } + if (isPersistedOnlyProperty(value)) { + // quarkus properties values always resolved from persisted config source + return value.withValue(PersistedConfigSource.getInstance().getValue(name)); + } + if (value.getValue().indexOf("${") == -1) { return value; } @@ -59,4 +70,19 @@ public class PropertyMappingInterceptor implements ConfigSourceInterceptor { return prop.getValue(); })); } + + private boolean isPersistedOnlyProperty(ConfigValue value) { + if (isQuarkusPropertiesEnabled && value.getName().startsWith(NS_QUARKUS)) { + String configSourceName = value.getConfigSourceName(); + + return Environment.isRuntimeMode() + && configSourceName != null + && !configSourceName.equals(PersistedConfigSource.NAME) + && !configSourceName.equals(AbstractRawDefaultConfigSource.NAME) + && !configSourceName.contains("Runtime Defaults") + && !configSourceName.contains("application.properties"); + } + + return false; + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/QuarkusPropertiesConfigSource.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/QuarkusPropertiesConfigSource.java new file mode 100644 index 0000000000..3e26a9707e --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/QuarkusPropertiesConfigSource.java @@ -0,0 +1,140 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.configuration; + +import static java.lang.Boolean.parseBoolean; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getRawPersistedProperty; +import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS; + +import java.io.File; +import java.io.IOException; +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.ArrayList; +import java.util.Collections; +import java.util.List; +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.ConfigValue; +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.common.utils.ConfigSourceUtil; + +/** + * A configuration source for {@code quarkus.properties}. + */ +public final class QuarkusPropertiesConfigSource extends AbstractLocationConfigSourceLoader implements ConfigSourceProvider { + + private static final String NAME = "QuarkusProperties"; + private static final String FILE_NAME = "quarkus.properties"; + public static final String QUARKUS_PROPERTY_ENABLED = "kc.quarkus-properties-enabled"; + + public static boolean isSameSource(ConfigValue value) { + if (value == null) { + return false; + } + + return NAME.equals(value.getConfigSourceName()); + } + + public static boolean isQuarkusPropertiesEnabled() { + return parseBoolean(getRawPersistedProperty(QUARKUS_PROPERTY_ENABLED).orElse(Boolean.FALSE.toString())); + } + + public static Path getConfigurationFile() { + String homeDir = Environment.getHomeDir(); + + if (homeDir != null) { + File file = Paths.get(homeDir, "conf", FILE_NAME).toFile(); + + if (file.exists()) { + return file.toPath(); + } + } + + return null; + } + + @Override + protected String[] getFileExtensions() { + return new String[] { "properties" }; + } + + @Override + protected ConfigSource loadConfigSource(URL url, int ordinal) throws IOException { + return new PropertiesConfigSource(ConfigSourceUtil.urlToMap(url), FILE_NAME, ordinal) { + @Override + public String getName() { + return NAME; + } + + @Override + public String getValue(String propertyName) { + if (propertyName.startsWith(NS_QUARKUS)) { + String value = super.getValue(propertyName); + + if (value == null) { + return PersistedConfigSource.getInstance().getValue(propertyName); + } + + return value; + } + + return null; + } + }; + } + + @Override + public List getConfigSources(final ClassLoader classLoader) { + List configSources = new ArrayList<>(); + + configSources.addAll(loadConfigSources("META-INF/services/" + FILE_NAME, 450, classLoader)); + + if (Environment.isRebuild() || Environment.isRebuildCheck()) { + Path configFile = getConfigurationFile(); + + if (configFile != null) { + configSources.addAll(loadConfigSources(configFile.toUri().toString(), 500, classLoader)); + } + } + + return configSources; + } + + @Override + protected List tryClassPath(URI uri, int ordinal, ClassLoader classLoader) { + try { + 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; + } + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java index 0360f1f241..2cae71d736 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java @@ -49,7 +49,7 @@ public final class PropertyMappers { } public static boolean isBuildTimeProperty(String name) { - if (isFeaturesBuildTimeProperty(name) || isSpiBuildTimeProperty(name)) { + if (isFeaturesBuildTimeProperty(name) || isSpiBuildTimeProperty(name) || name.startsWith(MicroProfileConfigProvider.NS_QUARKUS)) { return true; } @@ -123,6 +123,9 @@ public final class PropertyMappers { } public static PropertyMapper getMapper(String property) { + if (property.startsWith("%")) { + return MAPPERS.get(property.substring(property.indexOf('.') + 1)); + } return MAPPERS.get(property); } diff --git a/quarkus/runtime/src/main/resources/META-INF/keycloak.conf b/quarkus/runtime/src/main/resources/META-INF/keycloak.conf index 88b124d117..757e91c8d4 100644 --- a/quarkus/runtime/src/main/resources/META-INF/keycloak.conf +++ b/quarkus/runtime/src/main/resources/META-INF/keycloak.conf @@ -23,8 +23,3 @@ metrics-enabled=false %import_export.hostname-strict=false %import_export.hostname-strict-https=false %import_export.cluster=local - -# Logging configuration. INFO is the default level for most of the categories -#quarkus.log.level = DEBUG -quarkus.log.category."org.jboss.resteasy.resteasy_jaxrs.i18n".level=WARN -quarkus.log.category."org.infinispan.transaction.lookup.JBossStandaloneJTAManagerLookup".level=WARN diff --git a/quarkus/runtime/src/main/resources/META-INF/services/quarkus.properties b/quarkus/runtime/src/main/resources/META-INF/services/quarkus.properties new file mode 100644 index 0000000000..7de6fece8e --- /dev/null +++ b/quarkus/runtime/src/main/resources/META-INF/services/quarkus.properties @@ -0,0 +1,23 @@ +# +# 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. +# + +# Default options that rely on Quarkus specific options and lacking proper support in Keycloak + +# Logging configuration. INFO is the default level for most of the categories +quarkus.log.level = INFO +quarkus.log.category."org.jboss.resteasy.resteasy_jaxrs.i18n".level=WARN +quarkus.log.category."org.infinispan.transaction.lookup.JBossStandaloneJTAManagerLookup".level=WARN diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java index 655f95d4d7..4d0b29fbd3 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java @@ -391,6 +391,15 @@ public class ConfigurationTest { assertEquals("my_secret=", config.getConfigValue("kc.db-password").getValue()); } + @Test + public void testResolvePropertyFromDefaultProfile() { + Environment.setProfile("import_export"); + assertEquals("false", createConfig().getConfigValue("kc.hostname-strict").getValue()); + + Environment.setProfile("prod"); + assertEquals("true", createConfig().getConfigValue("kc.hostname-strict").getValue()); + } + private Config.Scope initConfig(String... scope) { Config.init(new MicroProfileConfigProvider(createConfig())); return Config.scope(scope); diff --git a/quarkus/runtime/src/test/resources/META-INF/keycloak.conf b/quarkus/runtime/src/test/resources/META-INF/keycloak.conf index 3877047105..cbf9d0c37e 100644 --- a/quarkus/runtime/src/test/resources/META-INF/keycloak.conf +++ b/quarkus/runtime/src/test/resources/META-INF/keycloak.conf @@ -1,15 +1,2 @@ spi-hostname-default-frontend-url = ${keycloak.frontendUrl:http://filepropdefault.unittest} -%user-profile.spi-hostname-default-frontend-url = http://filepropprofile.unittest - -# Default Non-Production Grade Datasource -quarkus.datasource.db-kind=h2 -quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect -quarkus.datasource.jdbc.driver=org.h2.jdbcx.JdbcDataSource -quarkus.datasource.jdbc.url = jdbc:h2:file:${kc.home.dir:~}/data/keycloakdb;;AUTO_SERVER=TRUE -quarkus.datasource.username = sa -quarkus.datasource.password = keycloak -quarkus.datasource.jdbc.transactions=xa - -# For test nested properties -quarkus.datasource.foo = jdbc:h2:file:${kc.home.dir:${kc.db.url.path:~}}/data/keycloakdb -quarkus.datasource.bar = foo-${kc.prop3:${kc.prop4:${kc.prop5:def}-suffix}} +%user-profile.spi-hostname-default-frontend-url = http://filepropprofile.unittest \ No newline at end of file diff --git a/quarkus/runtime/src/test/resources/META-INF/services/quarkus.properties b/quarkus/runtime/src/test/resources/META-INF/services/quarkus.properties new file mode 100644 index 0000000000..ddbd346144 --- /dev/null +++ b/quarkus/runtime/src/test/resources/META-INF/services/quarkus.properties @@ -0,0 +1,10 @@ +# Default options that rely on Quarkus specific options and lacking proper support in Keycloak + +# Logging configuration. INFO is the default level for most of the categories +quarkus.log.level = INFO +quarkus.log.category."org.jboss.resteasy.resteasy_jaxrs.i18n".level=WARN +quarkus.log.category."org.infinispan.transaction.lookup.JBossStandaloneJTAManagerLookup".level=WARN + +# For test nested properties +quarkus.datasource.foo = jdbc:h2:file:${kc.home.dir:${kc.db.url.path:~}}/data/keycloakdb +quarkus.datasource.bar = foo-${kc.prop3:${kc.prop4:${kc.prop5:def}-suffix}} \ No newline at end of file diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/BeforeStartDistribution.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/BeforeStartDistribution.java new file mode 100644 index 0000000000..000cb56996 --- /dev/null +++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/BeforeStartDistribution.java @@ -0,0 +1,36 @@ +/* + * 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 java.util.function.Consumer; +import org.keycloak.it.utils.KeycloakDistribution; + +/** + * {@link BeforeStartDistribution} is used to perform additional steps prior to starting the distribution. + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface BeforeStartDistribution { + + Class> value(); + +} 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 a036bee541..3ae1fb19da 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 @@ -81,4 +81,20 @@ public interface CLIResult extends LaunchResult { default void assertBuild() { assertMessage("Server configuration updated and persisted"); } + + default void assertNoBuild() { + assertFalse(getOutput().contains("Server configuration updated and persisted")); + } + + default boolean isClustered() { + return getOutput().contains("Starting JGroups channel `ISPN`"); + } + + default void assertLocalCache() { + assertFalse(isClustered()); + } + + default void assertClusteredCache() { + assertTrue(isClustered()); + } } 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 28e55c64e6..b40b795952 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 @@ -83,6 +83,10 @@ public class CLITestExtension extends QuarkusMainTestExtension { if (dist == null) { dist = createDistribution(distConfig); } + + onBeforeStartDistribution(context.getRequiredTestClass().getAnnotation(BeforeStartDistribution.class)); + onBeforeStartDistribution(context.getRequiredTestMethod().getAnnotation(BeforeStartDistribution.class)); + dist.start(Arrays.asList(launch.value())); } } else { @@ -94,6 +98,16 @@ public class CLITestExtension extends QuarkusMainTestExtension { } } + private void onBeforeStartDistribution(BeforeStartDistribution annotation) { + if (annotation != null) { + try { + annotation.value().getDeclaredConstructor().newInstance().accept(dist); + } catch (Exception cause) { + throw new RuntimeException("Error when invoking " + annotation.value() + " instance before starting distribution", cause); + } + } + } + @Override public void afterEach(ExtensionContext context) throws Exception { DistributionTest distConfig = getDistributionConfig(context); diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/DistributionLifecycleManager.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/DistributionLifecycleManager.java new file mode 100644 index 0000000000..3b9207becb --- /dev/null +++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/DistributionLifecycleManager.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.junit5.extension; + +import org.keycloak.it.utils.KeycloakDistribution; + +public interface DistributionLifecycleManager { + + void beforeStart(D 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 452e072f5e..ef172a82fc 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 @@ -38,4 +38,16 @@ public interface KeycloakDistribution { return commands.toArray(new String[0]); } + + default void setQuarkusProperty(String key, String value) { + throw new RuntimeException("Not implemented"); + } + + default void setProperty(String key, String value) { + throw new RuntimeException("Not implemented"); + } + + default void deleteQuarkusProperties() { + throw new RuntimeException("Not implemented"); + } } 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 index 55c9d3d138..4702c11a09 100644 --- 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 @@ -19,6 +19,8 @@ package org.keycloak.it.utils; import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; @@ -32,6 +34,7 @@ import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; +import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -60,6 +63,7 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { private boolean debug; private boolean reCreate; private ExecutorService outputExecutor; + private boolean inited = false; public RawKeycloakDistribution(boolean debug, boolean manualStop, boolean reCreate) { this.debug = debug; @@ -254,7 +258,7 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { 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()) { + if (!inited || (reCreate || !distPath.toFile().exists())) { distPath.toFile().delete(); ZipUtils.unzip(distFile.toPath(), distRootPath); } @@ -264,6 +268,8 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { throw new RuntimeException("Cannot set kc.sh executable"); } + inited = true; + return distPath; } catch (Exception cause) { throw new RuntimeException("Failed to prepare distribution", cause); @@ -311,4 +317,51 @@ public final class RawKeycloakDistribution implements KeycloakDistribution { keycloak = builder.start(); } + + @Override + public void setProperty(String key, String value) { + setProperty(key, value, distPath.resolve("conf").resolve("keycloak.conf").toFile()); + } + + @Override + public void setQuarkusProperty(String key, String value) { + setProperty(key, value, getQuarkusPropertiesFile()); + } + + @Override + public void deleteQuarkusProperties() { + File file = getQuarkusPropertiesFile(); + + if (file.exists()) { + file.delete(); + } + } + + private void setProperty(String key, String value, File confFile) { + Properties properties = new Properties(); + + if (confFile.exists()) { + try ( + FileInputStream in = new FileInputStream(confFile); + ) { + + properties.load(in); + } catch (Exception e) { + throw new RuntimeException("Failed to update " + confFile, e); + } + } + + try ( + FileOutputStream out = new FileOutputStream(confFile) + ) { + properties.put(key, value); + properties.store(out, ""); + } catch (Exception e) { + throw new RuntimeException("Failed to update " + confFile, e); + } + } + + private File getQuarkusPropertiesFile() { + return distPath.resolve("conf").resolve("quarkus.properties").toFile(); + } } diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java index 6c9cae8055..77ca07de40 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java @@ -17,18 +17,16 @@ package org.keycloak.it.cli.dist; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.MethodOrderer; +import java.util.function.Consumer; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.condition.DisabledIf; -import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.keycloak.it.junit5.extension.BeforeStartDistribution; import org.keycloak.it.junit5.extension.CLIResult; import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.junit5.extension.RawDistOnly; +import org.keycloak.it.utils.KeycloakDistribution; import io.quarkus.test.junit.main.Launch; import io.quarkus.test.junit.main.LaunchResult; @@ -41,14 +39,45 @@ public class BuildAndStartDistTest { @Test @Launch({ "build", "--cache=local" }) @Order(1) - void firstYouBuild(LaunchResult result) { + void testBuildWithCliArgs(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertBuild(); } @Test @Launch({ "start", "--http-enabled=true", "--hostname-strict=false" }) @Order(2) - void thenYouStart(LaunchResult result) { + void testStartUsingCliArgs(LaunchResult result) { CLIResult cliResult = (CLIResult) result; cliResult.assertStarted(); + cliResult.assertLocalCache(); + } + + @Test + @BeforeStartDistribution(SetDefaultOptions.class) + @Launch({ "build" }) + @Order(3) + void testBuildUsingConfFile(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertBuild(); + } + + @Test + @Launch({ "start" }) + @Order(4) + void testStartUsingConfFile(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertStarted(); + cliResult.assertLocalCache(); + } + + public static class SetDefaultOptions implements Consumer { + + @Override + public void accept(KeycloakDistribution distribution) { + distribution.setProperty("http-enabled", "true"); + distribution.setProperty("hostname-strict", "false"); + distribution.setProperty("cache", "local"); + } } } diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ClusterConfigDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ClusterConfigDistTest.java index 1bdf9f8852..ab288c263d 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ClusterConfigDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ClusterConfigDistTest.java @@ -35,7 +35,8 @@ public class ClusterConfigDistTest { @Test @Launch({ "start-dev", "--cache=ispn" }) void changeClusterSetting(LaunchResult result) { - assertTrue(isClustered(result)); + CLIResult cliResult = (CLIResult) result; + cliResult.assertClusteredCache(); } @Test @@ -61,7 +62,7 @@ public class ClusterConfigDistTest { void testExplicitCacheConfigFile(LaunchResult result) { CLIResult cliResult = (CLIResult) result; cliResult.assertStartedDevMode(); - assertTrue(isClustered(cliResult)); + cliResult.assertClusteredCache(); } @Test @@ -69,7 +70,7 @@ public class ClusterConfigDistTest { void testStartDefaultsToClustering(LaunchResult result) { CLIResult cliResult = (CLIResult) result; cliResult.assertStarted(); - assertTrue(isClustered(result)); + cliResult.assertClusteredCache(); } @Test @@ -77,10 +78,6 @@ public class ClusterConfigDistTest { void testStartDevDefaultsToLocalCaches(LaunchResult result) { CLIResult cliResult = (CLIResult) result; cliResult.assertStartedDevMode(); - assertFalse(isClustered(result)); - } - - private boolean isClustered(LaunchResult result) { - return result.getOutput().contains("Starting JGroups channel `ISPN`"); + cliResult.assertLocalCache(); } } diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/QuarkusPropertiesAutoBuildDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/QuarkusPropertiesAutoBuildDistTest.java new file mode 100644 index 0000000000..123138eb2f --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/QuarkusPropertiesAutoBuildDistTest.java @@ -0,0 +1,89 @@ +/* + * 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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.function.Consumer; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.keycloak.it.junit5.extension.BeforeStartDistribution; +import org.keycloak.it.junit5.extension.CLIResult; +import org.keycloak.it.junit5.extension.DistributionTest; +import org.keycloak.it.junit5.extension.RawDistOnly; +import org.keycloak.it.utils.KeycloakDistribution; + +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; + +@DistributionTest(reInstall = DistributionTest.ReInstall.NEVER) +@BeforeStartDistribution(QuarkusPropertiesAutoBuildDistTest.SetDebugLogLevel.class) +@RawDistOnly(reason = "Containers are immutable") +@TestMethodOrder(OrderAnnotation.class) +public class QuarkusPropertiesAutoBuildDistTest { + + @Test + @Launch({ "start", "--auto-build", "--http-enabled=true", "--hostname-strict=false", "--cache=local" }) + @Order(1) + void testReAugOnFirstRun(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertBuild(); + cliResult.assertMessage("DEBUG ["); + cliResult.assertStarted(); + } + + @Test + @Launch({ "start", "--auto-build", "--http-enabled=true", "--hostname-strict=false", "--cache=local" }) + @Order(2) + void testSecondStartDoNotTriggerReAug(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertNoBuild(); + cliResult.assertMessage("DEBUG ["); + cliResult.assertStarted(); + } + + @Test + @BeforeStartDistribution(SetInfoLogLevel.class) + @Launch({ "start", "--auto-build", "--http-enabled=true", "--hostname-strict=false", "--cache=local" }) + @Order(3) + void testReAugAfterChangingProperty(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertBuild(); + assertFalse(cliResult.getOutput().contains("DEBUG [")); + } + + public static class SetDebugLogLevel implements Consumer { + + @Override + public void accept(KeycloakDistribution distribution) { + distribution.setQuarkusProperty("quarkus.log.level", "DEBUG"); + } + } + + public static class SetInfoLogLevel implements Consumer { + + @Override + public void accept(KeycloakDistribution distribution) { + distribution.setQuarkusProperty("quarkus.log.level", "INFO"); + } + } + +} \ No newline at end of file diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/QuarkusPropertiesDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/QuarkusPropertiesDistTest.java new file mode 100644 index 0000000000..bbed2a0623 --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/QuarkusPropertiesDistTest.java @@ -0,0 +1,104 @@ +/* + * 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.assertFalse; + +import java.util.function.Consumer; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.keycloak.it.junit5.extension.BeforeStartDistribution; +import org.keycloak.it.junit5.extension.CLIResult; +import org.keycloak.it.junit5.extension.DistributionTest; +import org.keycloak.it.junit5.extension.RawDistOnly; +import org.keycloak.it.utils.KeycloakDistribution; + +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; + +@DistributionTest(reInstall = DistributionTest.ReInstall.NEVER) +@BeforeStartDistribution(QuarkusPropertiesDistTest.SetDebugLogLevel.class) +@RawDistOnly(reason = "Containers are immutable") +@TestMethodOrder(OrderAnnotation.class) +public class QuarkusPropertiesDistTest { + + @Test + @Launch({ "build", "--cache=local" }) + @Order(1) + void testBuildWithPropertyFromQuarkusProperties(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertMessage("DEBUG ["); + cliResult.assertBuild(); + } + + @Test + @Launch({ "start", "--http-enabled=true", "--hostname-strict=false" }) + @Order(2) + void testPropertyEnabledAtRuntime(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertMessage("DEBUG ["); + cliResult.assertStarted(); + } + + @Test + @Launch({ "-Dquarkus.log.level=INFO", "start", "--http-enabled=true", "--hostname-strict=false" }) + @Order(3) + void testIgnoreQuarkusSystemPropertiesAtStart(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertMessage("DEBUG ["); + cliResult.assertStarted(); + } + + @Test + @Launch({ "-Dquarkus.log.level=INFO", "build" }) + @Order(4) + void testIgnoreQuarkusSystemPropertyAtBuild(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertMessage("DEBUG ["); + cliResult.assertBuild(); + } + + @Test + @BeforeStartDistribution(SetDebugLogLevelInKeycloakConf.class) + @Launch({ "build" }) + @Order(5) + void testIgnoreQuarkusPropertyFromKeycloakConf(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + assertFalse(cliResult.getOutput().contains("DEBUG [")); + cliResult.assertBuild(); + } + + public static class SetDebugLogLevel implements Consumer { + + @Override + public void accept(KeycloakDistribution distribution) { + distribution.setQuarkusProperty("quarkus.log.level", "DEBUG"); + } + } + + public static class SetDebugLogLevelInKeycloakConf implements Consumer { + + @Override + public void accept(KeycloakDistribution distribution) { + distribution.deleteQuarkusProperties(); + distribution.setProperty("quarkus.log.level", "DEBUG"); + } + } +} \ No newline at end of file diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartAutoBuildDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartAutoBuildDistTest.java new file mode 100644 index 0000000000..ed7674357a --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartAutoBuildDistTest.java @@ -0,0 +1,79 @@ +/* + * 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.assertFalse; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.keycloak.it.junit5.extension.CLIResult; +import org.keycloak.it.junit5.extension.DistributionTest; +import org.keycloak.it.junit5.extension.RawDistOnly; + +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; + +@DistributionTest(reInstall = DistributionTest.ReInstall.NEVER) +@RawDistOnly(reason = "Containers are immutable") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class StartAutoBuildDistTest { + + @Test + @Launch({ "start", "--auto-build", "--http-enabled=true", "--hostname-strict=false", "--cache=local" }) + @Order(1) + void testStartAutoBuild(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertMessage("Changes detected in configuration. Updating the server image."); + cliResult.assertMessage("Updating the configuration and installing your custom providers, if any. Please wait."); + cliResult.assertMessage("Server configuration updated and persisted. Run the following command to review the configuration:"); + cliResult.assertMessage("kc.sh show-config"); + cliResult.assertMessage("Next time you run the server, just run:"); + cliResult.assertMessage("kc.sh start --http-enabled=true --hostname-strict=false"); + assertFalse(cliResult.getOutput().contains("--cache")); + cliResult.assertStarted(); + } + + @Test + @Launch({ "start", "--auto-build", "--http-enabled=true", "--hostname-strict=false", "--cache=local" }) + @Order(2) + void testShouldNotReAugIfConfigIsSame(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertNoBuild(); + cliResult.assertStarted(); + } + + @Test + @Launch({ "start", "--auto-build", "--db=h2-mem", "--http-enabled=true", "--hostname-strict=false", "--cache=local" }) + @Order(3) + void testShouldReAugIfConfigChanged(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertBuild(); + cliResult.assertStarted(); + } + + @Test + @Launch({ "start", "--auto-build", "--db=h2-mem", "--http-enabled=true", "--hostname-strict=false", "--cache=local" }) + @Order(4) + void testShouldNotReAugIfSameDatabase(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertNoBuild(); + cliResult.assertStarted(); + } +}