diff --git a/pom.xml b/pom.xml index 31543354a..b05dfbbc1 100644 --- a/pom.xml +++ b/pom.xml @@ -30,11 +30,9 @@ yyyy-MM-dd HH:mm - 2.2.0 5.3.2 3.0.0 5.0.0 - 27.0.0 7.8.0 3.13.2 4.2.17.Final @@ -227,7 +225,7 @@ org.wiremock - wiremock-jetty12 + wiremock-standalone ${wiremock.version} test @@ -262,20 +260,13 @@ test - + io.fabric8 - openshift-client + kubernetes-client ${kubernetes.fabric8.java.version} - - io.kubernetes - client-java - ${kubernetes.client.java.version} - test - - io.micronaut micronaut-http-client @@ -298,12 +289,25 @@ org.mockito mockito-core test + + + + net.bytebuddy + byte-buddy-agent + + org.mockito mockito-junit-jupiter test + + + net.bytebuddy + byte-buddy-agent + + @@ -313,15 +317,6 @@ test - - - uk.org.webcompere - system-stubs-core - 2.1.8 - test - - javax.xml.bind jaxb-api @@ -359,21 +354,6 @@ - - com.cloudogu.versionName - processor - ${versionNameVersion} - - provided - - - - org.awaitility - awaitility - 4.3.0 - test - - io.github.classgraph classgraph @@ -403,6 +383,17 @@ + + + src/main/resources + + + src/main/version + com/cloudogu/gitops/cli + true + + + com.diffplug.spotless @@ -517,16 +508,7 @@ micronaut-inject-java ${micronaut.version} - - com.cloudogu.versionName - processor - ${versionNameVersion} - - - - -AversionName=${versionName} - diff --git a/src/main/java/com/cloudogu/gitops/cli/ApplicationConfigurator.java b/src/main/java/com/cloudogu/gitops/cli/ApplicationConfigurator.java index cedf56398..8d46f79c3 100644 --- a/src/main/java/com/cloudogu/gitops/cli/ApplicationConfigurator.java +++ b/src/main/java/com/cloudogu/gitops/cli/ApplicationConfigurator.java @@ -2,17 +2,25 @@ import com.cloudogu.gitops.config.Config; import com.cloudogu.gitops.config.Credentials; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.io.UncheckedIOException; import java.net.MalformedURLException; import java.net.URI; +import java.util.function.Function; -@RequiredArgsConstructor @Slf4j public class ApplicationConfigurator { + private final Function environment; + + public ApplicationConfigurator() { + this(System::getenv); + } + + ApplicationConfigurator(Function environment) { + this.environment = environment; + } private static boolean hasText(String value) { return value != null && !value.isEmpty(); @@ -128,7 +136,7 @@ private static void addRegistryConfig(Config newConfig) { } private void addAdditionalApplicationConfig(Config newConfig) { - if (System.getenv("KUBERNETES_SERVICE_HOST") != null) { + if (environment.apply("KUBERNETES_SERVICE_HOST") != null) { log.debug("installation is running in kubernetes."); newConfig.getApplication().setRunningInsideK8s(true); } @@ -350,8 +358,8 @@ public boolean isUrlSetAndValid(Config config) { public void buildAndValidateURLFromEnvironment(Config config) { log.debug("Attempting to set features.argocd.resourceInclusionsCluster via Kubernetes ENV variables."); - String host = System.getenv("KUBERNETES_SERVICE_HOST"); - String port = System.getenv("KUBERNETES_SERVICE_PORT"); + String host = environment.apply("KUBERNETES_SERVICE_HOST"); + String port = environment.apply("KUBERNETES_SERVICE_PORT"); String errorMessage = "Could not determine 'features.argocd.resourceInclusionsCluster' which is required when argocd.operator=true. " + "Ensure Kubernetes environment variables 'KUBERNETES_SERVICE_HOST' and 'KUBERNETES_SERVICE_PORT' are set properly. " + "Alternatively, try setting 'features.argocd.resourceInclusionsCluster' in the config to manually override."; diff --git a/src/main/java/com/cloudogu/gitops/cli/Version.java b/src/main/java/com/cloudogu/gitops/cli/Version.java new file mode 100644 index 000000000..2f9324dbd --- /dev/null +++ b/src/main/java/com/cloudogu/gitops/cli/Version.java @@ -0,0 +1,27 @@ +package com.cloudogu.gitops.cli; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; + +public final class Version { + + private static final String VERSION_RESOURCE = "/com/cloudogu/gitops/cli/version-name.txt"; + + public static final String NAME = loadName(); + + private Version() { + } + + private static String loadName() { + try (InputStream input = Version.class.getResourceAsStream(VERSION_RESOURCE)) { + if (input == null) { + throw new IllegalStateException("Version resource not found: " + VERSION_RESOURCE); + } + return new String(input.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read version resource: " + VERSION_RESOURCE, e); + } + } +} diff --git a/src/main/java/com/cloudogu/gitops/cli/package-info.java b/src/main/java/com/cloudogu/gitops/cli/package-info.java deleted file mode 100644 index 590a10787..000000000 --- a/src/main/java/com/cloudogu/gitops/cli/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Creates class Version during build via annotation processing - */ -@VersionName(packageName = "com.cloudogu.gitops.cli") -package com.cloudogu.gitops.cli; - -import com.cloudogu.versionname.VersionName; diff --git a/src/main/java/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClient.java b/src/main/java/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClient.java index 90e0d01aa..a48ae06ad 100644 --- a/src/main/java/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClient.java +++ b/src/main/java/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClient.java @@ -7,6 +7,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.api.model.GenericKubernetesResourceBuilder; import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.IntOrString; @@ -32,9 +33,6 @@ import io.fabric8.kubernetes.client.dsl.base.PatchContext; import io.fabric8.kubernetes.client.dsl.base.ResourceDefinitionContext; import io.fabric8.kubernetes.client.utils.Serialization; -import io.fabric8.openshift.api.model.Project; -import io.fabric8.openshift.api.model.ProjectBuilder; -import io.fabric8.openshift.client.OpenShiftClient; import jakarta.inject.Singleton; import lombok.Getter; import lombok.Setter; @@ -78,6 +76,13 @@ public class K8sClient { private static final String DOCKER_CONFIG_JSON_KEY = ".dockerconfigjson"; private static final String NOT_FOUND_IN_NAMESPACE = " not found in namespace "; private static final String APPLIED_PREFIX = "Applied "; + private static final ResourceDefinitionContext OPENSHIFT_PROJECT_CONTEXT = new ResourceDefinitionContext.Builder() + .withGroup("project.openshift.io") + .withVersion("v1") + .withKind("Project") + .withPlural("projects") + .withNamespaced(false) + .build(); private static final int DEFAULT_TIMEOUT_SECONDS = 60; private static final int DEFAULT_CHECK_INTERVAL_SECONDS = 1; @@ -337,12 +342,16 @@ public void createNamespace(String name) { log.debug("Namespace {} does not exist, proceeding to create.", name); if (runInOpenshift()) { - OpenShiftClient osClient = client.adapt(OpenShiftClient.class); - - Project project = new ProjectBuilder().withNewMetadata().withName(name).endMetadata().build(); + GenericKubernetesResource project = new GenericKubernetesResourceBuilder() + .withApiVersion("project.openshift.io/v1") + .withKind("Project") + .withNewMetadata() + .withName(name) + .endMetadata() + .build(); executeWithErrorHandling( "create project " + name, () -> { - osClient.projects().resource(project).create(); + client.genericKubernetesResources(OPENSHIFT_PROJECT_CONTEXT).resource(project).create(); return null; } ); diff --git a/src/main/version/version-name.txt b/src/main/version/version-name.txt new file mode 100644 index 000000000..117d4ef2a --- /dev/null +++ b/src/main/version/version-name.txt @@ -0,0 +1 @@ +${versionName} \ No newline at end of file diff --git a/src/test/java/com/cloudogu/gitops/application/content/ContentLoaderTest.java b/src/test/java/com/cloudogu/gitops/application/content/ContentLoaderTest.java index 19ba62309..1af5e2015 100644 --- a/src/test/java/com/cloudogu/gitops/application/content/ContentLoaderTest.java +++ b/src/test/java/com/cloudogu/gitops/application/content/ContentLoaderTest.java @@ -18,6 +18,7 @@ import com.cloudogu.gitops.testhelper.git.TestScmManagerApiClient; import com.cloudogu.gitops.tools.common.HelmChartConfig; import com.cloudogu.gitops.tools.core.Jenkins; +import com.cloudogu.gitops.tools.core.JenkinsToolConfigMapper; import com.cloudogu.gitops.utils.FileSystemUtils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; @@ -58,7 +59,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -78,7 +78,7 @@ class ContentLoaderTest { private final CredentialsResolver credentialsResolver = new CredentialsResolver(k8sClient); private final TestGitRepoFactory scmmRepoProvider = new TestGitRepoFactory(config, new FileSystemUtils()); private final TestScmManagerApiClient scmmApiClient = new TestScmManagerApiClient(config); - private final Jenkins jenkins = mock(Jenkins.class); + private final JenkinsForTest jenkins = new JenkinsForTest(); private final ScmManagerProviderMock scmManagerMock = new ScmManagerProviderMock(); private final GitHandler gitHandler = new GitHandlerForTests(scmManagerMock); private final Deployer deployer = mock(Deployer.class); @@ -965,6 +965,7 @@ void initCommonRepoExpectUnchangedRepo() throws IOException, GitAPIException { @Test void ensureJenkinsJobWillBeCreated() { + config.getJenkins().setActive(true); config.getContent().setRepos(List.of(repository(repo -> { repo.setUrl(createContentRepo("copyRepo1")); repo.setRef("main"); @@ -973,15 +974,15 @@ void ensureJenkinsJobWillBeCreated() { repo.setTarget("common/repo"); }))); scmmApiClient.mockRepoApiBehaviour(); - when(jenkins.isEnabled(any(DeploymentContext.class))).thenReturn(true); install(createContent(config), config); - verify(jenkins).createJenkinsjob(any(), any()); + assertThat(jenkins.createdJobs).containsExactly("common/common"); } @Test void ensureJenkinsJobCreationWillBeIgnored() { + config.getJenkins().setActive(true); config.getContent().setRepos(List.of(repository(repo -> { repo.setUrl(createContentRepo("copyRepo1")); repo.setRef("main"); @@ -990,28 +991,27 @@ void ensureJenkinsJobCreationWillBeIgnored() { repo.setTarget("common/repo"); }))); scmmApiClient.mockRepoApiBehaviour(); - when(jenkins.isEnabled(any(DeploymentContext.class))).thenReturn(false); install(createContent(config), config); - verify(jenkins, never()).createJenkinsjob(any(), any()); + assertThat(jenkins.createdJobs).isEmpty(); } @Test void ensureJenkinsJobWillNotBeCreatedIfJenkinsIsNotEnabled() { + config.getJenkins().setActive(false); config.getContent().setRepos(List.of(repository(repo -> { repo.setUrl(createContentRepo("copyRepo1")); repo.setRef("main"); repo.setType(ContentRepoType.COPY); - repo.setCreateJenkinsJob(false); + repo.setCreateJenkinsJob(true); repo.setTarget("common/repo"); }))); scmmApiClient.mockRepoApiBehaviour(); - when(jenkins.isEnabled(any(DeploymentContext.class))).thenReturn(false); install(createContent(config), config); - verify(jenkins, never()).createJenkinsjob(any(), any()); + assertThat(jenkins.createdJobs).isEmpty(); } @Test @@ -1428,6 +1428,37 @@ private static Object readPrivateField(Object target, String fieldName) throws R return field.get(target); } + class JenkinsForTest extends Jenkins { + + private final List createdJobs = new ArrayList<>(); + + JenkinsForTest() { + super( + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + new JenkinsToolConfigMapper(config), + null, + null, + null + ); + } + + @Override + public void createJenkinsjob(String namespace, String repoName) { + createdJobs.add(namespace + "/" + repoName); + } + } + class ContentLoaderForTest extends ContentLoader { private final Config contentConfig; diff --git a/src/test/java/com/cloudogu/gitops/cli/ApplicationConfiguratorTest.java b/src/test/java/com/cloudogu/gitops/cli/ApplicationConfiguratorTest.java index c05e71217..6315c58b4 100644 --- a/src/test/java/com/cloudogu/gitops/cli/ApplicationConfiguratorTest.java +++ b/src/test/java/com/cloudogu/gitops/cli/ApplicationConfiguratorTest.java @@ -34,7 +34,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariable; class ApplicationConfiguratorTest { @@ -76,7 +75,7 @@ class ApplicationConfiguratorTest { @BeforeEach void setup() { fileSystemUtils = new FileSystemUtils(); - applicationConfigurator = new ApplicationConfigurator(); + applicationConfigurator = configuratorWithEnvironment(Map.of()); testLogger = new TestLogger(applicationConfigurator.getClass()); commonFeatureConfig = new CommonToolConfig(); @@ -126,11 +125,12 @@ void correctConfigWithNoProgramArguments() { } @Test - void setsConfigApplicationRunningInsideK8s() throws Exception { - withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "127.0.0.1").execute(() -> { - Config actualConfig = applicationConfigurator.initConfig(testConfig); - assertThat(actualConfig.getApplication().getRunningInsideK8s()).isEqualTo(true); - }); + void setsConfigApplicationRunningInsideK8s() { + applicationConfigurator = configuratorWithEnvironment(Map.of("KUBERNETES_SERVICE_HOST", "127.0.0.1")); + + Config actualConfig = applicationConfigurator.initConfig(testConfig); + + assertThat(actualConfig.getApplication().getRunningInsideK8s()).isEqualTo(true); } @Test @@ -603,6 +603,21 @@ void shouldValidateAndAcceptUserProvidedValidResourceInclusionsClusterUrl() { )).isNotEmpty(); } + @Test + void userProvidedResourceInclusionsClusterTrumpsEnvironmentVariables() { + applicationConfigurator = configuratorWithEnvironment(Map.of( + "KUBERNETES_SERVICE_HOST", "100.125.0.1", + "KUBERNETES_SERVICE_PORT", "443" + )); + testConfig.getFeatures().getArgocd().setOperator(true); + testConfig.getFeatures().getArgocd().setResourceInclusionsCluster("https://192.168.0.1:6443"); + + applicationConfigurator.initConfig(testConfig); + + assertThat(testConfig.getFeatures().getArgocd().getResourceInclusionsCluster()) + .isEqualTo("https://192.168.0.1:6443"); + } + @Test void shouldThrowExceptionForUserProvidedInvalidResourceInclusionsClusterUrl() { testConfig.getFeatures().getArgocd().setOperator(true); @@ -619,21 +634,21 @@ void shouldThrowExceptionForUserProvidedInvalidResourceInclusionsClusterUrl() { } @Test - void shouldSetResourceInclusionsClusterUsingKubernetesEnvVariablesWhenNotProvidedByUser() throws Exception { + void shouldSetResourceInclusionsClusterUsingKubernetesEnvVariablesWhenNotProvidedByUser() { + applicationConfigurator = configuratorWithEnvironment(Map.of( + "KUBERNETES_SERVICE_HOST", "127.0.0.1", + "KUBERNETES_SERVICE_PORT", "6443" + )); testConfig.getFeatures().getArgocd().setOperator(true); testConfig.getFeatures().getArgocd().setResourceInclusionsCluster(null); - withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "127.0.0.1") - .and("KUBERNETES_SERVICE_PORT", "6443") - .execute(() -> { - Config actualConfig = applicationConfigurator.initConfig(testConfig); + Config actualConfig = applicationConfigurator.initConfig(testConfig); - assertThat(actualConfig.getFeatures().getArgocd().getResourceInclusionsCluster()) - .isEqualTo("https://127.0.0.1:6443"); - assertThat(testLogger.getLogs().search( - "Successfully set features.argocd.resourceInclusionsCluster via Kubernetes ENV to: https://127.0.0.1:6443" - )).isNotEmpty(); - }); + assertThat(actualConfig.getFeatures().getArgocd().getResourceInclusionsCluster()) + .isEqualTo("https://127.0.0.1:6443"); + assertThat(testLogger.getLogs().search( + "Successfully set features.argocd.resourceInclusionsCluster via Kubernetes ENV to: https://127.0.0.1:6443" + )).isNotEmpty(); } @Test @@ -678,28 +693,32 @@ void shouldThrowExceptionWhenKubernetesEnvVariablesAreNotSetAndResourceInclusion } @Test - void shouldThrowExceptionForInvalidKubernetesConstructedUrl() throws Exception { + void shouldThrowExceptionForInvalidKubernetesConstructedUrl() { + applicationConfigurator = configuratorWithEnvironment(Map.of( + "KUBERNETES_SERVICE_HOST", "invalid_host", + "KUBERNETES_SERVICE_PORT", "not_a_port" + )); testConfig.getFeatures().getArgocd().setOperator(true); testConfig.getFeatures().getArgocd().setResourceInclusionsCluster(null); - withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "invalid_host") - .and("KUBERNETES_SERVICE_PORT", "not_a_port") - .execute(() -> { - RuntimeException exception = assertThrows( - RuntimeException.class, - () -> applicationConfigurator.initConfig(testConfig) - ); + RuntimeException exception = assertThrows( + RuntimeException.class, + () -> applicationConfigurator.initConfig(testConfig) + ); - assertThat(exception.getMessage()).contains( - "Could not determine 'features.argocd.resourceInclusionsCluster' which is required when argocd.operator=true." - ); - }); + assertThat(exception.getMessage()).contains( + "Could not determine 'features.argocd.resourceInclusionsCluster' which is required when argocd.operator=true." + ); assertThat(testLogger.getLogs().search( "Constructed internal Kubernetes API Server URL: https://invalid_host:not_a_port" )).isNotEmpty(); } + private static ApplicationConfigurator configuratorWithEnvironment(Map environment) { + return new ApplicationConfigurator(environment::get); + } + @Test void setsAllToolNamespacesToApplicationNamespaceWhenConfigured() { Config config = minimalConfig(); diff --git a/src/test/java/com/cloudogu/gitops/cli/VersionTest.java b/src/test/java/com/cloudogu/gitops/cli/VersionTest.java new file mode 100644 index 000000000..9a14f18c6 --- /dev/null +++ b/src/test/java/com/cloudogu/gitops/cli/VersionTest.java @@ -0,0 +1,17 @@ +package com.cloudogu.gitops.cli; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class VersionTest { + + @Test + void loadsGeneratedVersionName() { + assertThat(Version.NAME) + .isNotBlank() + .doesNotContain("${") + .contains("Copyright 2020 - present Cloudogu GmbH") + .contains("GNU AFFERO GENERAL PUBLIC LICENSE, Version 3"); + } +} diff --git a/src/test/java/com/cloudogu/gitops/dependencyinjection/okhttp/RetryInterceptorTest.java b/src/test/java/com/cloudogu/gitops/dependencyinjection/okhttp/RetryInterceptorTest.java index c70d7c7ce..9d4c325fc 100644 --- a/src/test/java/com/cloudogu/gitops/dependencyinjection/okhttp/RetryInterceptorTest.java +++ b/src/test/java/com/cloudogu/gitops/dependencyinjection/okhttp/RetryInterceptorTest.java @@ -1,7 +1,9 @@ package com.cloudogu.gitops.dependencyinjection.okhttp; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import okhttp3.Interceptor; import okhttp3.OkHttpClient; +import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; import org.junit.jupiter.api.BeforeEach; @@ -12,6 +14,7 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.IOException; +import java.net.SocketTimeoutException; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.security.cert.X509Certificate; @@ -24,6 +27,10 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class RetryInterceptorTest { @@ -100,28 +107,25 @@ void retriesThreeTimesOn500WithHttps() throws IOException, GeneralSecurityExcept } @Test - void retriesOnTimeout() throws IOException, GeneralSecurityException { - String path = "/timeout-test"; - - wireMock.stubFor(get(urlEqualTo(path)) - .inScenario("Timeout Scenario") - .whenScenarioStateIs("Started") - .willReturn(aResponse() - .withStatus(200) - .withFixedDelay(2000)) - .willSetStateTo("After Timeout")); + void retriesOnTimeout() throws IOException { + Request request = new Request.Builder().url("http://localhost/timeout-test").build(); + Interceptor.Chain chain = mock(Interceptor.Chain.class); + Response successfulResponse = new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .build(); - wireMock.stubFor(get(urlEqualTo(path)) - .inScenario("Timeout Scenario") - .whenScenarioStateIs("After Timeout") - .willReturn(aResponse() - .withStatus(200) - .withBody("Successful Result"))); + when(chain.request()).thenReturn(request); + when(chain.proceed(request)) + .thenThrow(new SocketTimeoutException("Read timed out")) + .thenReturn(successfulResponse); - OkHttpClient client = createClient(100); - Response response = client.newCall(new Request.Builder().url(wireMock.baseUrl() + path).build()).execute(); - assertThat(response.body().string()).isEqualTo("Successful Result"); - wireMock.verify(2, getRequestedFor(urlEqualTo(path))); + try (Response response = new RetryInterceptor(3, 0).intercept(chain)) { + assertThat(response.code()).isEqualTo(200); + } + verify(chain, times(2)).proceed(request); } @Test diff --git a/src/test/java/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClientTest.java b/src/test/java/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClientTest.java index f0a91090d..cc2c9dc87 100644 --- a/src/test/java/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClientTest.java +++ b/src/test/java/com/cloudogu/gitops/infrastructure/kubernetes/api/K8sClientTest.java @@ -19,7 +19,6 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; -import io.fabric8.openshift.api.model.ProjectBuilder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -451,7 +450,9 @@ void createNamespaceCreatesOpenShiftProjectWhenOpenshiftConfigIsEnabled() throws .post() .withPath("/apis/project.openshift.io/v1/projects") .andReturn( - 201, new ProjectBuilder() + 201, new GenericKubernetesResourceBuilder() + .withApiVersion("project.openshift.io/v1") + .withKind("Project") .withNewMetadata() .withName("test-project") .endMetadata() diff --git a/src/test/java/com/cloudogu/gitops/integration/Polling.java b/src/test/java/com/cloudogu/gitops/integration/Polling.java new file mode 100644 index 000000000..466fa1c0a --- /dev/null +++ b/src/test/java/com/cloudogu/gitops/integration/Polling.java @@ -0,0 +1,65 @@ +package com.cloudogu.gitops.integration; + +import java.time.Duration; +import java.time.Instant; +import java.util.function.BooleanSupplier; + +public final class Polling { + + private Polling() { + } + + public static void until(BooleanSupplier condition, Duration timeout, Duration pollInterval) { + untilAsserted(() -> { + if (!condition.getAsBoolean()) { + throw new AssertionError("Condition is not fulfilled"); + } + }, timeout, pollInterval); + } + + public static void untilAsserted(Runnable assertion, Duration timeout, Duration pollInterval) { + if (timeout.isNegative()) { + throw new IllegalArgumentException("Timeout must not be negative"); + } + if (pollInterval.isNegative()) { + throw new IllegalArgumentException("Poll interval must not be negative"); + } + + Instant deadline = Instant.now().plus(timeout); + Throwable lastFailure; + do { + try { + assertion.run(); + return; + } catch (RuntimeException | AssertionError failure) { + lastFailure = failure; + } + + sleepUntilNextAttempt(pollInterval, deadline); + } while (Instant.now().isBefore(deadline)); + + throw new TimeoutException(timeout, lastFailure); + } + + private static void sleepUntilNextAttempt(Duration pollInterval, Instant deadline) { + long remainingMillis = Duration.between(Instant.now(), deadline).toMillis(); + if (remainingMillis <= 0) { + return; + } + + long sleepMillis = Math.min(pollInterval.toMillis(), remainingMillis); + try { + Thread.sleep(sleepMillis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while waiting for condition", e); + } + } + + public static final class TimeoutException extends RuntimeException { + + private TimeoutException(Duration timeout, Throwable cause) { + super("Condition was not fulfilled within " + timeout, cause); + } + } +} diff --git a/src/test/java/com/cloudogu/gitops/integration/PollingTest.java b/src/test/java/com/cloudogu/gitops/integration/PollingTest.java new file mode 100644 index 000000000..7e10fc331 --- /dev/null +++ b/src/test/java/com/cloudogu/gitops/integration/PollingTest.java @@ -0,0 +1,57 @@ +package com.cloudogu.gitops.integration; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PollingTest { + + @Test + void retriesUntilAssertionSucceeds() { + AtomicInteger attempts = new AtomicInteger(); + + Polling.untilAsserted( + () -> assertThat(attempts.incrementAndGet()).isGreaterThanOrEqualTo(3), + Duration.ofSeconds(1), + Duration.ZERO + ); + + assertThat(attempts).hasValue(3); + } + + @Test + void reportsLastFailureOnTimeout() { + assertThatThrownBy(() -> Polling.untilAsserted( + () -> { + throw new AssertionError("not ready"); + }, + Duration.ZERO, + Duration.ZERO + )) + .isInstanceOf(Polling.TimeoutException.class) + .hasCauseInstanceOf(AssertionError.class) + .hasRootCauseMessage("not ready"); + } + + @Test + void restoresInterruptStatus() { + Thread.currentThread().interrupt(); + try { + assertThatThrownBy(() -> Polling.until( + () -> false, + Duration.ofSeconds(1), + Duration.ofMillis(1) + )) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Interrupted while waiting for condition") + .hasCauseInstanceOf(InterruptedException.class); + assertThat(Thread.currentThread().isInterrupted()).isTrue(); + } finally { + Thread.interrupted(); + } + } +} diff --git a/src/test/java/com/cloudogu/gitops/integration/TestK8sHelper.java b/src/test/java/com/cloudogu/gitops/integration/TestK8sHelper.java index 2e938db44..98b109153 100644 --- a/src/test/java/com/cloudogu/gitops/integration/TestK8sHelper.java +++ b/src/test/java/com/cloudogu/gitops/integration/TestK8sHelper.java @@ -11,10 +11,10 @@ import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; import lombok.extern.slf4j.Slf4j; -import org.awaitility.Awaitility; import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; @@ -165,10 +165,11 @@ public void onClose(int code, String reason) { .usingListener(listener) .exec(cmd)) { - Awaitility.await() - .atMost(5, TimeUnit.MINUTES) - .pollInterval(500, TimeUnit.MILLISECONDS) - .until(() -> finished.getCount() == 0); + Polling.until( + () -> finished.getCount() == 0, + Duration.ofMinutes(5), + Duration.ofMillis(500) + ); } catch (Exception e) { throw new RuntimeException("Exec failed/timeout for pod " + ns + "/" + pod, e); } @@ -255,10 +256,11 @@ public static boolean waitForAllPodsRunningInNamespace( int timeout, TimeUnit timeoutUnit ) { - Awaitility.await() - .atMost(timeout, timeoutUnit) - .pollInterval(DEFAULT_POLL_SECONDS, TimeUnit.SECONDS) - .untilAsserted(() -> checkAllPodsRunningInNamespace(namespace, podNameStartsWith)); + Polling.untilAsserted( + () -> checkAllPodsRunningInNamespace(namespace, podNameStartsWith), + Duration.of(timeout, timeoutUnit.toChronoUnit()), + Duration.ofSeconds(DEFAULT_POLL_SECONDS) + ); return true; } @@ -343,10 +345,11 @@ public static boolean waitForPodPrefixesRunningInNamespace( int timeout, TimeUnit timeoutUnit ) { - Awaitility.await() - .atMost(timeout, timeoutUnit) - .pollInterval(DEFAULT_POLL_SECONDS, TimeUnit.SECONDS) - .untilAsserted(() -> checkPodPrefixesRunningInNamespace(namespace, expectedPodPrefixes)); + Polling.untilAsserted( + () -> checkPodPrefixesRunningInNamespace(namespace, expectedPodPrefixes), + Duration.of(timeout, timeoutUnit.toChronoUnit()), + Duration.ofSeconds(DEFAULT_POLL_SECONDS) + ); return true; } @@ -434,10 +437,11 @@ public static boolean waitForPodsMatchingRunningInNamespace( int timeout, TimeUnit timeoutUnit ) { - Awaitility.await() - .atMost(timeout, timeoutUnit) - .pollInterval(DEFAULT_POLL_SECONDS, TimeUnit.SECONDS) - .untilAsserted(() -> checkPodsMatchingRunningInNamespace(namespace, expectedPods)); + Polling.untilAsserted( + () -> checkPodsMatchingRunningInNamespace(namespace, expectedPods), + Duration.of(timeout, timeoutUnit.toChronoUnit()), + Duration.ofSeconds(DEFAULT_POLL_SECONDS) + ); return true; } @@ -482,10 +486,11 @@ public static boolean waitForNamespaces( int timeout, TimeUnit timeoutUnit ) { - Awaitility.await() - .atMost(timeout, timeoutUnit) - .pollInterval(DEFAULT_POLL_SECONDS, TimeUnit.SECONDS) - .untilAsserted(() -> checkNamespacesExist(expectedNamespaces)); + Polling.untilAsserted( + () -> checkNamespacesExist(expectedNamespaces), + Duration.of(timeout, timeoutUnit.toChronoUnit()), + Duration.ofSeconds(DEFAULT_POLL_SECONDS) + ); return true; } diff --git a/src/test/java/com/cloudogu/gitops/integration/profiles/ArgoCDOperatorProfileTestIT.java b/src/test/java/com/cloudogu/gitops/integration/profiles/ArgoCDOperatorProfileTestIT.java index 6c0e64835..e9f317c5e 100644 --- a/src/test/java/com/cloudogu/gitops/integration/profiles/ArgoCDOperatorProfileTestIT.java +++ b/src/test/java/com/cloudogu/gitops/integration/profiles/ArgoCDOperatorProfileTestIT.java @@ -1,17 +1,16 @@ package com.cloudogu.gitops.integration.profiles; +import com.cloudogu.gitops.integration.Polling; import com.cloudogu.gitops.integration.TestK8sHelper; import io.fabric8.kubernetes.api.model.Namespace; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.KubernetesClientException; -import org.awaitility.Awaitility; -import org.awaitility.core.ConditionTimeoutException; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfSystemProperty; -import java.util.concurrent.TimeUnit; +import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -31,19 +30,18 @@ public class ArgoCDOperatorProfileTestIT extends ProfileTestSetup { static void labelTest() { System.out.println("###### Integration ArgoCD Operator test ######"); try { - Awaitility.await() - .atMost(40, TimeUnit.MINUTES) - .pollInterval(5, TimeUnit.SECONDS) - .untilAsserted(() -> assertThat( - TestK8sHelper.checkAllPodsRunningInNamespace( + Polling.until( + () -> TestK8sHelper.checkAllPodsRunningInNamespace( namespaceOperator, "argocd-operator-controller" ) && TestK8sHelper.checkAllPodsRunningInNamespace( namespaceArgocd, "argocd-server" - ) - ).isTrue()); - } catch (ConditionTimeoutException timeoutEx) { + ), + Duration.ofMinutes(40), + Duration.ofSeconds(5) + ); + } catch (Polling.TimeoutException timeoutEx) { TestK8sHelper.dumpNamespacesAndPods(); fail("Cluster not ready, sth false."); } diff --git a/src/test/java/com/cloudogu/gitops/integration/profiles/MandantProfileTestIT.java b/src/test/java/com/cloudogu/gitops/integration/profiles/MandantProfileTestIT.java index 6c7ecddec..e4d80c99f 100644 --- a/src/test/java/com/cloudogu/gitops/integration/profiles/MandantProfileTestIT.java +++ b/src/test/java/com/cloudogu/gitops/integration/profiles/MandantProfileTestIT.java @@ -1,19 +1,19 @@ package com.cloudogu.gitops.integration.profiles; +import com.cloudogu.gitops.integration.Polling; import com.cloudogu.gitops.integration.TestK8sHelper; import io.fabric8.kubernetes.api.model.Namespace; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.KubernetesClientException; import lombok.extern.slf4j.Slf4j; -import org.awaitility.Awaitility; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import java.time.Duration; import java.util.List; -import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -42,18 +42,17 @@ static void labelMyTest() { private static void waitUntilTenantIsReady() { // tenant is created very late after running GOP twice! - Awaitility.await() - .atMost(40, TimeUnit.MINUTES) - .pollInterval(5, TimeUnit.SECONDS) - .untilAsserted(() -> assertThat( - TestK8sHelper.checkAllPodsRunningInNamespace( + Polling.until( + () -> TestK8sHelper.checkAllPodsRunningInNamespace( TENANT_NAMESPACE_REGISTRY, "docker-registry" ) && TestK8sHelper.checkAllPodsRunningInNamespace( TENANT_NAMESPACE_SCM, "scmm-" - ) - ).isTrue()); + ), + Duration.ofMinutes(40), + Duration.ofSeconds(5) + ); } @DisabledIfSystemProperty(named = "micronaut.environments", matches = "operator-mandants") diff --git a/src/test/java/com/cloudogu/gitops/integration/profiles/PetclinicProfileTestIT.java b/src/test/java/com/cloudogu/gitops/integration/profiles/PetclinicProfileTestIT.java index 72e1bf29e..02bf5e991 100644 --- a/src/test/java/com/cloudogu/gitops/integration/profiles/PetclinicProfileTestIT.java +++ b/src/test/java/com/cloudogu/gitops/integration/profiles/PetclinicProfileTestIT.java @@ -1,5 +1,6 @@ package com.cloudogu.gitops.integration.profiles; +import com.cloudogu.gitops.integration.Polling; import com.cloudogu.gitops.integration.TestK8sHelper; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.networking.v1.Ingress; @@ -8,7 +9,6 @@ import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.KubernetesClientException; import lombok.extern.slf4j.Slf4j; -import org.awaitility.core.ConditionTimeoutException; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; @@ -38,7 +38,7 @@ static void labelTest() { try { waitForContentExamplePrerequisites(); TestK8sHelper.waitForAllPodsRunningInNamespace(exampleStagingNs, "", 40, TimeUnit.MINUTES); - } catch (ConditionTimeoutException timeoutEx) { + } catch (Polling.TimeoutException timeoutEx) { TestK8sHelper.dumpNamespacesAndPods(); fail("Cluster not ready, sth false.", timeoutEx); } diff --git a/src/test/java/com/cloudogu/gitops/integration/profiles/PrefixProfileTestIT.java b/src/test/java/com/cloudogu/gitops/integration/profiles/PrefixProfileTestIT.java index b57c3f01c..cfe7acef6 100644 --- a/src/test/java/com/cloudogu/gitops/integration/profiles/PrefixProfileTestIT.java +++ b/src/test/java/com/cloudogu/gitops/integration/profiles/PrefixProfileTestIT.java @@ -1,8 +1,8 @@ package com.cloudogu.gitops.integration.profiles; +import com.cloudogu.gitops.integration.Polling; import com.cloudogu.gitops.integration.TestK8sHelper; import lombok.extern.slf4j.Slf4j; -import org.awaitility.core.ConditionTimeoutException; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfSystemProperty; @@ -39,7 +39,7 @@ static void labelTest() { try { TestK8sHelper.waitForAllPodsRunningInNamespace(certManagerNs, "", 40, TimeUnit.MINUTES); - } catch (ConditionTimeoutException timeoutEx) { + } catch (Polling.TimeoutException timeoutEx) { TestK8sHelper.dumpNamespacesAndPods(); fail("Cluster not ready, sth false.", timeoutEx); } diff --git a/src/test/java/com/cloudogu/gitops/integration/tools/KubernetesApiTestSetup.java b/src/test/java/com/cloudogu/gitops/integration/tools/KubernetesApiTestSetup.java index d20e4712e..103b6fd0e 100644 --- a/src/test/java/com/cloudogu/gitops/integration/tools/KubernetesApiTestSetup.java +++ b/src/test/java/com/cloudogu/gitops/integration/tools/KubernetesApiTestSetup.java @@ -1,76 +1,27 @@ package com.cloudogu.gitops.integration.tools; -import io.kubernetes.client.openapi.ApiClient; -import io.kubernetes.client.openapi.Configuration; -import io.kubernetes.client.openapi.apis.CoreV1Api; -import io.kubernetes.client.util.ClientBuilder; -import io.kubernetes.client.util.KubeConfig; -import org.junit.jupiter.api.BeforeAll; +import com.cloudogu.gitops.integration.Polling; import org.junit.jupiter.api.BeforeEach; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; import java.time.Duration; -import java.time.Instant; -import java.util.function.Supplier; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; public abstract class KubernetesApiTestSetup { - static String kubeConfigPath; - CoreV1Api api; int TIME_TO_WAIT = 12; int RETRY_SECONDS = 30; /** - * Gets path to kubeconfig. - */ - @BeforeAll - static void setupKubeconfig() { - kubeConfigPath = System.getenv("HOME") + "/.kube/config"; - if (!new File(kubeConfigPath).exists()) { - kubeConfigPath = System.getenv("KUBECONFIG"); - } - assertThat(kubeConfigPath).isNotBlank(); - } - - /** - * establish connection to kubernetes and create API to use. + * Waits until the Kubernetes resources required by the integration test are ready. */ @BeforeEach - void setupConnection() throws IOException { - ApiClient client = ClientBuilder.kubeconfig(KubeConfig.loadKubeConfig(new FileReader(kubeConfigPath))).build(); - // set the global default api-client to the out-of-cluster one from above - Configuration.setDefaultApiClient(client); - - // the CoreV1Api loads default api-client from global configuration. - api = new CoreV1Api(); - waitForCondition( + void waitUntilReady() { + Polling.until( this::waitingCondition, maxWaitTimeInMinutes(TIME_TO_WAIT), pollIntervallSeconds(RETRY_SECONDS) ); } - static void waitForCondition(Supplier condition, Duration timeout, Duration pollInterval) { - Instant end = Instant.now().plus(timeout); - while (Instant.now().isBefore(end)) { - if (condition.get()) { - return; - } - try { - Thread.sleep(pollInterval.toMillis()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("break polling", e); - } - } - fail("Wait condition not fulfilled in time"); - } - private Duration pollIntervallSeconds(int time) { return Duration.ofSeconds(time); } diff --git a/src/test/java/com/cloudogu/gitops/integration/tools/MonitoringTestIT.java b/src/test/java/com/cloudogu/gitops/integration/tools/MonitoringTestIT.java index fe6b9d5f7..33f16d1c8 100644 --- a/src/test/java/com/cloudogu/gitops/integration/tools/MonitoringTestIT.java +++ b/src/test/java/com/cloudogu/gitops/integration/tools/MonitoringTestIT.java @@ -1,9 +1,9 @@ package com.cloudogu.gitops.integration.tools; import com.cloudogu.gitops.integration.TestK8sHelper; -import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1Pod; -import io.kubernetes.client.openapi.models.V1PodList; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -59,13 +59,12 @@ void ensureOperatorIsStarted() { @Disabled("not start on jenkins") @Test - void ensureMonitoringIsStarted() throws ApiException { - V1PodList pods = api.listNamespacedPod(namespace).execute(); - assertThat(pods).isNotNull(); - assertThat(pods.getItems().isEmpty()).isFalse(); + void ensureMonitoringIsStarted() { + List pods = listPods(); + assertThat(pods).isNotEmpty(); - V1Pod prometheus = null; - for (V1Pod pod : pods.getItems()) { + Pod prometheus = null; + for (Pod pod : pods) { if (pod.getMetadata().getName().contains(prometheusPod)) { prometheus = pod; break; @@ -77,8 +76,13 @@ void ensureMonitoringIsStarted() throws ApiException { @Disabled("jenkins got only 2") @Test - void ensureNamespaceGot3Pods() throws ApiException { - V1PodList pods = api.listNamespacedPod(namespace).execute(); - assertThat(pods.getItems().size()).isEqualTo(3); + void ensureNamespaceGot3Pods() { + assertThat(listPods()).hasSize(3); + } + + private List listPods() { + try (KubernetesClient client = new KubernetesClientBuilder().build()) { + return client.pods().inNamespace(namespace).list().getItems(); + } } } diff --git a/src/test/java/com/cloudogu/gitops/tools/core/argocd/ArgoCDConfigurationTest.java b/src/test/java/com/cloudogu/gitops/tools/core/argocd/ArgoCDConfigurationTest.java index 9d98b928d..d8881ada1 100644 --- a/src/test/java/com/cloudogu/gitops/tools/core/argocd/ArgoCDConfigurationTest.java +++ b/src/test/java/com/cloudogu/gitops/tools/core/argocd/ArgoCDConfigurationTest.java @@ -43,7 +43,6 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariable; @EnableKubernetesMockClient(crud = true) class ArgoCDConfigurationTest { @@ -852,30 +851,6 @@ void configuresResourceInclusionsCluster() throws IOException { } } - @Test - void resourceInclusionsClusterFromConfigTrumpsEnvironmentVariables() throws Exception { - ArgoCD argocd = setupOperatorTest(false); - config.getApplication().setInternalKubernetesApiUrl("https://192.168.0.1:6443"); - - withEnvironmentVariable("KUBERNETES_SERVICE_HOST", "100.125.0.1") - .and("KUBERNETES_SERVICE_PORT", "443") - .execute(() -> execute(argocd)); - - clusterResourcesRepoLayout = ((ArgoCDForTest) argocd).getClusterRepoLayout(); - - Map yaml = parseActualYaml(clusterResourcesRepoLayout.operatorConfigFile()); - String expectedClusterUrlFromConfig = "https://192.168.0.1:6443"; - String resourceInclusions = (String) value(yaml, "spec", "resourceInclusions"); - List> parsedResourceInclusions = parseYamlList(resourceInclusions); - - for (Map resource : parsedResourceInclusions) { - assertThat(resource).containsKey("clusters"); - assertThat(listValue(resource, "clusters")) - .contains(expectedClusterUrlFromConfig) - .doesNotContain("https://100.125.0.1:443"); - } - } - @Test void setsEnvironmentVariablesInArgoCdComponentsWhenProvided() throws IOException { ArgoCD argocd = setupOperatorTest(false); diff --git a/src/test/java/com/cloudogu/gitops/tools/core/scmmanager/ScmManagerSetupTest.java b/src/test/java/com/cloudogu/gitops/tools/core/scmmanager/ScmManagerSetupTest.java index 7769a3058..1ca5ea5ab 100644 --- a/src/test/java/com/cloudogu/gitops/tools/core/scmmanager/ScmManagerSetupTest.java +++ b/src/test/java/com/cloudogu/gitops/tools/core/scmmanager/ScmManagerSetupTest.java @@ -20,6 +20,8 @@ import com.cloudogu.gitops.utils.Tuple; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import okhttp3.internal.http.RealResponseBody; +import okio.BufferedSource; import org.eclipse.jgit.api.errors.GitAPIException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -311,11 +313,12 @@ void stopsWaitingWhenInterrupted() throws IOException { @SuppressWarnings("unchecked") Call apiCall = mock(Call.class); - @SuppressWarnings("unchecked") - Response response = mock(Response.class); + Response response = Response.error( + 503, + new RealResponseBody("text/plain", 0, mock(BufferedSource.class)) + ); when(generalApi.checkScmmAvailable()).thenReturn(apiCall); when(apiCall.execute()).thenReturn(response); - when(response.isSuccessful()).thenReturn(false); ScmManagerSetup scmManagerSetup = new ScmManagerSetup( scmManager, diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..fdbd0b157 --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-subclass