Skip to content
Open
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,10 @@ Name | Description
`TEST_PROXY_PORT` | _(Optional)_ The port of a proxy to route all requests through. Defaults to `8080`.
`TEST_PROXY_USERNAME` | _(Optional)_ The username for a proxy to route all requests through
`TEST_SKIPSSLVALIDATION` | _(Optional)_ Whether to skip SSL validation when connecting to the Cloud Foundry instance. Defaults to `false`.
`SKIP_BROWSER_AUTH_TESTS` | _(Optional)_ Set to `true` to skip integration tests that require browser-based authentication (e.g., SSO tests). Useful when the Cloud Foundry instance's UAA does not have browser-based authentication configured. Defaults to `false`.
`SKIP_METRIC_REGISTRAR_TESTS` | _(Optional)_ Set to `true` to skip integration tests that require the Metric Registrar service (e.g., MetricRegistrarTest). Useful when the Cloud Foundry instance does not have the Metric Registrar service available. Defaults to `false`.
`SKIP_TCP_ROUTING_TESTS` | _(Optional)_ Set to `true` to skip TCP routing integration tests (TcpRoutesTest, RouterGroupsTest). Useful when the Cloud Foundry instance does not have TCP routing configured (i.e., no `routing_endpoint` in the info payload). Defaults to `false`.
`SKIP_V2_TESTS` | _(Optional)_ Set to `true` to skip all V2 API integration tests. Useful when the Cloud Foundry V2 API is rate-limited or unavailable. When enabled, tests annotated with `@RequiresV2Api` will be skipped, including V3 tests that depend on V2 API calls (e.g., service broker creation). Defaults to `false`.

If you do not have access to a CloudFoundry instance with admin access, you can run one locally using [bosh-deployment](https://github.com/cloudfoundry/bosh-deployment) & [cf-deployment](https://github.com/cloudfoundry/cf-deployment/) and Virtualbox.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import java.util.function.Supplier;
import javax.net.ssl.SSLException;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.client.v2.applications.ListApplicationServiceBindingsRequest;
import org.cloudfoundry.client.v2.applications.RemoveApplicationServiceBindingRequest;
import org.cloudfoundry.client.v2.buildpacks.DeleteBuildpackRequest;
import org.cloudfoundry.client.v2.buildpacks.ListBuildpacksRequest;
Expand Down Expand Up @@ -77,8 +76,11 @@
import org.cloudfoundry.client.v3.Metadata;
import org.cloudfoundry.client.v3.Relationship;
import org.cloudfoundry.client.v3.applications.Application;
import org.cloudfoundry.client.v3.applications.ApplicationResource;
import org.cloudfoundry.client.v3.applications.DeleteApplicationRequest;
import org.cloudfoundry.client.v3.applications.ListApplicationsRequest;
import org.cloudfoundry.client.v3.servicebindings.DeleteServiceBindingRequest;
import org.cloudfoundry.client.v3.servicebindings.ListServiceBindingsRequest;
import org.cloudfoundry.client.v3.serviceinstances.ListSharedSpacesRelationshipRequest;
import org.cloudfoundry.client.v3.serviceinstances.ListSharedSpacesRelationshipResponse;
import org.cloudfoundry.client.v3.serviceinstances.UnshareServiceInstanceRequest;
Expand Down Expand Up @@ -129,6 +131,8 @@ final class CloudFoundryCleaner implements InitializingBean, DisposableBean {

private static final Logger LOGGER = LoggerFactory.getLogger("cloudfoundry-client.test");

private static final boolean RUN_V2_CLEANUP = RequiresV2Api.V2ApiCondition.isEnabled();

private static final Map<String, Boolean> STANDARD_FEATURE_FLAGS =
FluentMap.<String, Boolean>builder()
.entry("app_bits_upload", true)
Expand Down Expand Up @@ -186,6 +190,30 @@ public void destroy() {
}

void clean() {
if (!RUN_V2_CLEANUP) {
LOGGER.info("Skipping V2 API cleanup operations (SKIP_V2_TESTS=true)");
// Only run V3 and UAA cleanup operations
Flux.empty()
.thenMany(
Mono.when( // No prerequisites - V3/UAA only
cleanClients(this.uaaClient, this.nameFactory),
cleanGroups(this.uaaClient, this.nameFactory),
cleanIdentityProviders(this.uaaClient, this.nameFactory),
cleanIdentityZones(this.uaaClient, this.nameFactory)))
.thenMany(
Mono.when(
cleanApplicationsV3(
this.cloudFoundryClient,
this.nameFactory),
cleanUsers(this.uaaClient, this.nameFactory)))
.retryWhen(Retry.max(5).filter(SSLException.class::isInstance))
.doOnSubscribe(s -> LOGGER.debug(">> CLEANUP (V3 only) <<"))
.doOnComplete(() -> LOGGER.debug("<< CLEANUP (V3 only) >>"))
.then()
.block(Duration.ofMinutes(30));
return;
}

Flux.empty()
.thenMany(
Mono.when( // Before Routes
Expand Down Expand Up @@ -218,8 +246,7 @@ void clean() {
Mono.when(
cleanApplicationsV3(
this.cloudFoundryClient,
this.nameFactory), // After Routes, cannot run with
// other cleanApps
this.nameFactory),
cleanUsers(this.uaaClient, this.nameFactory) // After CF Users
))
.thenMany(
Expand All @@ -243,30 +270,35 @@ void clean() {
private static Flux<Void> cleanApplicationsV3(
CloudFoundryClient cloudFoundryClient, NameFactory nameFactory) {
return PaginationUtils.requestClientV3Resources(
page ->
cloudFoundryClient
.applicationsV3()
.list(ListApplicationsRequest.builder().page(page).build()))
.filter(application -> nameFactory.isApplicationName(application.getName()))
page ->
cloudFoundryClient
.applicationsV3()
.list(
ListApplicationsRequest.builder()
.page(page)
.build()))
.filter(
application ->
nameFactory.isApplicationName(application.getName()))
.delayUntil(
application ->
removeApplicationServiceBindings(cloudFoundryClient, application))
removeApplicationServiceBindings(
cloudFoundryClient, application))
.flatMap(
application ->
cloudFoundryClient
.applicationsV3()
.delete(
DeleteApplicationRequest.builder()
.applicationId(application.getId())
.build())
.then()
.doOnError(
t ->
LOGGER.error(
"Unable to delete V3 application"
+ " {}",
application.getName(),
t)));
application ->
cloudFoundryClient
.applicationsV3()
.delete(
DeleteApplicationRequest.builder()
.applicationId(application.getId())
.build())
.then()
.doOnError(
t ->
LOGGER.error(
"Unable to delete V3 application" + " {}",
application.getName(),
t)));
}

private static Flux<Void> cleanBuildpacks(
Expand Down Expand Up @@ -1160,21 +1192,24 @@ private static boolean isCleanable(NameFactory nameFactory, UserResource resourc

private static Flux<Void> removeApplicationServiceBindings(
CloudFoundryClient cloudFoundryClient, Application application) {
return PaginationUtils.requestClientV2Resources(
return PaginationUtils.requestClientV3Resources(
page ->
cloudFoundryClient
.applicationsV2()
.listServiceBindings(
ListApplicationServiceBindingsRequest.builder()
.serviceBindingsV3()
.list(
ListServiceBindingsRequest.builder()
.page(page)
.applicationId(application.getId())
.build()))
.flatMap(
serviceBinding ->
requestRemoveServiceBinding(
cloudFoundryClient,
application.getId(),
ResourceUtils.getId(serviceBinding))
cloudFoundryClient
.serviceBindingsV3()
.delete(
DeleteServiceBindingRequest.builder()
.serviceBindingId(serviceBinding.getId())
.build())
.then()
.doOnError(
t ->
LOGGER.error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
Expand Down Expand Up @@ -346,6 +347,7 @@ RandomNameFactory nameFactory() {

@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
@ConditionalOnProperty(name = RequiresV2Api.SKIP_V2_TESTS_ENV, havingValue = "false", matchIfMissing = true)
Mono<String> metricRegistrarServiceInstance(
CloudFoundryClient cloudFoundryClient, Mono<String> spaceId, NameFactory nameFactory) {
return spaceId.flatMap(
Expand Down Expand Up @@ -373,6 +375,7 @@ NetworkingClient networkingClient(

@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
@ConditionalOnProperty(name = RequiresV2Api.SKIP_V2_TESTS_ENV, havingValue = "false", matchIfMissing = true)
Mono<String> organizationId(
CloudFoundryClient cloudFoundryClient,
String organizationName,
Expand Down Expand Up @@ -470,6 +473,7 @@ Version serverVersion(@Qualifier("admin") CloudFoundryClient cloudFoundryClient)
@Lazy
@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
@ConditionalOnProperty(name = RequiresV2Api.SKIP_V2_TESTS_ENV, havingValue = "false", matchIfMissing = true)
Mono<String> serviceBrokerId(
CloudFoundryClient cloudFoundryClient,
NameFactory nameFactory,
Expand Down Expand Up @@ -512,6 +516,7 @@ String serviceName(NameFactory nameFactory) {

@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
@ConditionalOnProperty(name = RequiresV2Api.SKIP_V2_TESTS_ENV, havingValue = "false", matchIfMissing = true)
Mono<String> spaceId(
CloudFoundryClient cloudFoundryClient, Mono<String> organizationId, String spaceName) {
return organizationId
Expand All @@ -538,6 +543,7 @@ String spaceName(NameFactory nameFactory) {

@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
@ConditionalOnProperty(name = RequiresV2Api.SKIP_V2_TESTS_ENV, havingValue = "false", matchIfMissing = true)
Mono<String> stackId(CloudFoundryClient cloudFoundryClient, Mono<String> stackName) {
return stackName
.flux()
Expand Down Expand Up @@ -566,6 +572,7 @@ Mono<String> stackId(CloudFoundryClient cloudFoundryClient, Mono<String> stackNa
*/
@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
@ConditionalOnProperty(name = RequiresV2Api.SKIP_V2_TESTS_ENV, havingValue = "false", matchIfMissing = true)
Mono<String> stackName(CloudFoundryClient cloudFoundryClient) {
return PaginationUtils.requestClientV2Resources(
page ->
Expand All @@ -583,6 +590,7 @@ Mono<String> stackName(CloudFoundryClient cloudFoundryClient) {
@Lazy
@Bean(initMethod = "block")
@DependsOn("cloudFoundryCleaner")
@ConditionalOnProperty(name = RequiresV2Api.SKIP_V2_TESTS_ENV, havingValue = "false", matchIfMissing = true)
Mono<ApplicationUtils.ApplicationMetadata> testLogCacheApp(
CloudFoundryClient cloudFoundryClient,
Mono<String> spaceId,
Expand Down Expand Up @@ -619,6 +627,7 @@ String testLogCacheAppName(NameFactory nameFactory) {

@Lazy
@Bean
@ConditionalOnProperty(name = RequiresV2Api.SKIP_V2_TESTS_ENV, havingValue = "false", matchIfMissing = true)
TestLogCacheEndpoints testLogCacheEndpoints(
ConnectionContext connectionContext,
TokenProvider tokenProvider,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2026 the original author or authors.
*
* 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.cloudfoundry;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;

/**
* Annotation to mark tests that require browser-based authentication (e.g., OAuth
* authorization code grants, implicit grants). Tests annotated with this will be skipped
* if the environment variable {@code SKIP_BROWSER_AUTH_TESTS} is set to "true".
*
* <p>Usage:
* <pre>
* &#64;RequiresBrowserAuth
* public class MyAuthTest extends AbstractIntegrationTest {
* // ...
* }
* </pre>
*
* <p>To skip browser auth tests, set the environment variable:
* <pre>
* export SKIP_BROWSER_AUTH_TESTS=true
* </pre>
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ExtendWith(RequiresBrowserAuth.BrowserAuthCondition.class)
public @interface RequiresBrowserAuth {

/**
* JUnit 5 ExecutionCondition that checks if Browser Auth tests should be skipped.
*/
class BrowserAuthCondition implements ExecutionCondition {

private static final String SKIP_BROWSER_AUTH_ENV = "SKIP_BROWSER_AUTH_TESTS";

@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
if ("true".equalsIgnoreCase(System.getenv(SKIP_BROWSER_AUTH_ENV))) {
return ConditionEvaluationResult.disabled(
"Tests requiring Browser Authentication are disabled via "
+ SKIP_BROWSER_AUTH_ENV
+ " environment variable");
}

return ConditionEvaluationResult.enabled("Browser authentication tests are enabled");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2026 the original author or authors.
*
* 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.cloudfoundry;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;

/**
* Annotation to mark tests that require the Metric Registrar service to be available.
* Tests annotated with this will be skipped if the environment variable
* {@code SKIP_METRIC_REGISTRAR_TESTS} is set to "true".
*
* <p>Usage:
* <pre>
* &#64;RequiresMetricRegistrar
* public class MetricTest extends AbstractIntegrationTest {
* // ...
* }
* </pre>
*
* <p>To skip metric registrar tests, set the environment variable:
* <pre>
* export SKIP_METRIC_REGISTRAR_TESTS=true
* </pre>
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ExtendWith(RequiresMetricRegistrar.RegistrarCondition.class)
public @interface RequiresMetricRegistrar {

/**
* JUnit 5 ExecutionCondition that checks if Metric Registrar tests should be skipped.
*/
class RegistrarCondition implements ExecutionCondition {

private static final String SKIP_REGISTRAR_ENV = "SKIP_METRIC_REGISTRAR_TESTS";

@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
if ("true".equalsIgnoreCase(System.getenv(SKIP_REGISTRAR_ENV))) {
return ConditionEvaluationResult.disabled(
"Tests requiring Metric Registrar Service are disabled via "
+ SKIP_REGISTRAR_ENV
+ " environment variable.");
}

return ConditionEvaluationResult.enabled("Metric Registrar tests are enabled");
}
}
}
Loading