diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/_ReactorCloudFoundryClient.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/_ReactorCloudFoundryClient.java index 6fe80b2d96..2b4ef40efc 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/_ReactorCloudFoundryClient.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/_ReactorCloudFoundryClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2021 the original author or authors. + * Copyright 2013-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. @@ -58,6 +58,7 @@ import org.cloudfoundry.client.v3.deployments.DeploymentsV3; import org.cloudfoundry.client.v3.domains.DomainsV3; import org.cloudfoundry.client.v3.droplets.Droplets; +import org.cloudfoundry.client.v3.featureflags.FeatureFlagsV3; import org.cloudfoundry.client.v3.isolationsegments.IsolationSegments; import org.cloudfoundry.client.v3.jobs.JobsV3; import org.cloudfoundry.client.v3.quotas.organizations.OrganizationQuotasV3; @@ -120,6 +121,7 @@ import org.cloudfoundry.reactor.client.v3.deployments.ReactorDeploymentsV3; import org.cloudfoundry.reactor.client.v3.domains.ReactorDomainsV3; import org.cloudfoundry.reactor.client.v3.droplets.ReactorDroplets; +import org.cloudfoundry.reactor.client.v3.featureflags.ReactorFeatureFlagsV3; import org.cloudfoundry.reactor.client.v3.isolationsegments.ReactorIsolationSegments; import org.cloudfoundry.reactor.client.v3.jobs.ReactorJobsV3; import org.cloudfoundry.reactor.client.v3.quotas.organizations.ReactorOrganizationQuotasV3; @@ -256,6 +258,12 @@ public FeatureFlags featureFlags() { return new ReactorFeatureFlags(getConnectionContext(), getRootV2(), getTokenProvider(), getRequestTags()); } + @Override + @Value.Derived + public FeatureFlagsV3 featureFlagsV3() { + return new ReactorFeatureFlagsV3(getConnectionContext(), getRootV3(), getTokenProvider(), getRequestTags()); + } + @Override @Value.Derived public Info info() { diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/featureflags/ReactorFeatureFlagsV3.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/featureflags/ReactorFeatureFlagsV3.java new file mode 100644 index 0000000000..30ac19fe90 --- /dev/null +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/featureflags/ReactorFeatureFlagsV3.java @@ -0,0 +1,80 @@ +/* + * 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.reactor.client.v3.featureflags; + +import java.util.Map; +import org.cloudfoundry.client.v3.featureflags.FeatureFlagsV3; +import org.cloudfoundry.client.v3.featureflags.GetFeatureFlagRequest; +import org.cloudfoundry.client.v3.featureflags.GetFeatureFlagResponse; +import org.cloudfoundry.client.v3.featureflags.ListFeatureFlagsRequest; +import org.cloudfoundry.client.v3.featureflags.ListFeatureFlagsResponse; +import org.cloudfoundry.client.v3.featureflags.UpdateFeatureFlagRequest; +import org.cloudfoundry.client.v3.featureflags.UpdateFeatureFlagResponse; +import org.cloudfoundry.reactor.ConnectionContext; +import org.cloudfoundry.reactor.TokenProvider; +import org.cloudfoundry.reactor.client.v3.AbstractClientV3Operations; +import reactor.core.publisher.Mono; + +/** + * The Reactor-based implementation of {@link FeatureFlagsV3} + */ +public final class ReactorFeatureFlagsV3 extends AbstractClientV3Operations + implements FeatureFlagsV3 { + + /** + * Creates an instance + * + * @param connectionContext the {@link ConnectionContext} to use when communicating with the server + * @param root the root URI of the server. Typically something like {@code https://api.run.pivotal.io}. + * @param tokenProvider the {@link TokenProvider} to use when communicating with the server + * @param requestTags map with custom http headers which will be added to web request + */ + public ReactorFeatureFlagsV3( + ConnectionContext connectionContext, + Mono root, + TokenProvider tokenProvider, + Map requestTags) { + super(connectionContext, root, tokenProvider, requestTags); + } + + @Override + public Mono get(GetFeatureFlagRequest request) { + return get( + request, + GetFeatureFlagResponse.class, + builder -> builder.pathSegment("feature_flags", request.getName())) + .checkpoint(); + } + + @Override + public Mono list(ListFeatureFlagsRequest request) { + return get( + request, + ListFeatureFlagsResponse.class, + builder -> builder.pathSegment("feature_flags")) + .checkpoint(); + } + + @Override + public Mono update(UpdateFeatureFlagRequest request) { + return patch( + request, + UpdateFeatureFlagResponse.class, + builder -> builder.pathSegment("feature_flags", request.getName())) + .checkpoint(); + } +} diff --git a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java index c28ea0d1a4..ea41c151c9 100644 --- a/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java +++ b/cloudfoundry-client-reactor/src/main/java/org/cloudfoundry/reactor/client/v3/packages/ReactorPackages.java @@ -128,7 +128,7 @@ public Mono listDroplets(ListPackageDropletsRequest public Mono upload(UploadPackageRequest request) { Path bits = request.getBits(); - if (bits.toFile().isDirectory()) { + if (bits != null && bits.toFile().isDirectory()) { return FileUtils.compress(bits) .map( temporaryFile -> diff --git a/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/featureflags/ReactorFeatureFlagsV3Test.java b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/featureflags/ReactorFeatureFlagsV3Test.java new file mode 100644 index 0000000000..82f54fd107 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/java/org/cloudfoundry/reactor/client/v3/featureflags/ReactorFeatureFlagsV3Test.java @@ -0,0 +1,174 @@ +/* + * 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.reactor.client.v3.featureflags; + +import static io.netty.handler.codec.http.HttpMethod.GET; +import static io.netty.handler.codec.http.HttpMethod.PATCH; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; + +import java.time.Duration; +import java.util.Collections; +import org.cloudfoundry.client.v3.Link; +import org.cloudfoundry.client.v3.Pagination; +import org.cloudfoundry.client.v3.featureflags.GetFeatureFlagRequest; +import org.cloudfoundry.client.v3.featureflags.GetFeatureFlagResponse; +import org.cloudfoundry.client.v3.featureflags.ListFeatureFlagsRequest; +import org.cloudfoundry.client.v3.featureflags.ListFeatureFlagsResponse; +import org.cloudfoundry.client.v3.featureflags.UpdateFeatureFlagRequest; +import org.cloudfoundry.client.v3.featureflags.UpdateFeatureFlagResponse; +import org.cloudfoundry.reactor.InteractionContext; +import org.cloudfoundry.reactor.TestRequest; +import org.cloudfoundry.reactor.TestResponse; +import org.cloudfoundry.reactor.client.AbstractClientApiTest; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +final class ReactorFeatureFlagsV3Test extends AbstractClientApiTest { + + private static final String BASE_URL = "https://api.example.org/v3/feature_flags/"; + private static final String UPDATED_AT = "2020-03-10T15:49:32Z"; + + private final ReactorFeatureFlagsV3 featureFlags = + new ReactorFeatureFlagsV3( + CONNECTION_CONTEXT, this.root, TOKEN_PROVIDER, Collections.emptyMap()); + + @Test + void get() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(GET) + .path("/feature_flags/app_scaling") + .build()) + .response( + TestResponse.builder() + .status(OK) + .payload( + "fixtures/client/v3/featureflags/GET_{feature_flag_name}_response.json") + .build()) + .build()); + + this.featureFlags + .get(GetFeatureFlagRequest.builder().name("app_scaling").build()) + .as(StepVerifier::create) + .expectNext( + GetFeatureFlagResponse.builder() + .name("app_scaling") + .enabled(true) + .updatedAt(UPDATED_AT) + .link("self", selfLink("app_scaling")) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void list() { + mockRequest( + InteractionContext.builder() + .request(TestRequest.builder().method(GET).path("/feature_flags").build()) + .response( + TestResponse.builder() + .status(OK) + .payload( + "fixtures/client/v3/featureflags/GET_response.json") + .build()) + .build()); + + this.featureFlags + .list(ListFeatureFlagsRequest.builder().build()) + .as(StepVerifier::create) + .expectNext( + ListFeatureFlagsResponse.builder() + .pagination( + Pagination.builder() + .totalResults(2) + .first( + Link.builder() + .href( + "https://api.example.org/v3/feature_flags?page=1&per_page=50") + .build()) + .last( + Link.builder() + .href( + "https://api.example.org/v3/feature_flags?page=1&per_page=50") + .build()) + .build()) + .resource( + GetFeatureFlagResponse.builder() + .name("app_scaling") + .enabled(true) + .updatedAt(UPDATED_AT) + .link("self", selfLink("app_scaling")) + .build()) + .resource( + GetFeatureFlagResponse.builder() + .name("user_org_creation") + .enabled(false) + .customErrorMessage("This feature is disabled") + .updatedAt(UPDATED_AT) + .link("self", selfLink("user_org_creation")) + .build()) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void update() { + mockRequest( + InteractionContext.builder() + .request( + TestRequest.builder() + .method(PATCH) + .path("/feature_flags/app_scaling") + .payload( + "fixtures/client/v3/featureflags/PATCH_{feature_flag_name}_request.json") + .build()) + .response( + TestResponse.builder() + .status(OK) + .payload( + "fixtures/client/v3/featureflags/PATCH_{feature_flag_name}_response.json") + .build()) + .build()); + + this.featureFlags + .update( + UpdateFeatureFlagRequest.builder() + .name("app_scaling") + .enabled(false) + .customErrorMessage("This feature is disabled") + .build()) + .as(StepVerifier::create) + .expectNext( + UpdateFeatureFlagResponse.builder() + .name("app_scaling") + .enabled(false) + .customErrorMessage("This feature is disabled") + .updatedAt(UPDATED_AT) + .link("self", selfLink("app_scaling")) + .build()) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + private static Link selfLink(String flagName) { + return Link.builder().href(BASE_URL + flagName).build(); + } +} diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/featureflags/GET_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/featureflags/GET_response.json new file mode 100644 index 0000000000..1f3b39c0d9 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/featureflags/GET_response.json @@ -0,0 +1,37 @@ +{ + "pagination": { + "total_results": 2, + "first": { + "href": "https://api.example.org/v3/feature_flags?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/feature_flags?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "name": "app_scaling", + "enabled": true, + "custom_error_message": null, + "updated_at": "2020-03-10T15:49:32Z", + "links": { + "self": { + "href": "https://api.example.org/v3/feature_flags/app_scaling" + } + } + }, + { + "name": "user_org_creation", + "enabled": false, + "custom_error_message": "This feature is disabled", + "updated_at": "2020-03-10T15:49:32Z", + "links": { + "self": { + "href": "https://api.example.org/v3/feature_flags/user_org_creation" + } + } + } + ] +} diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/featureflags/GET_{feature_flag_name}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/featureflags/GET_{feature_flag_name}_response.json new file mode 100644 index 0000000000..e4565f495a --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/featureflags/GET_{feature_flag_name}_response.json @@ -0,0 +1,11 @@ +{ + "name": "app_scaling", + "enabled": true, + "custom_error_message": null, + "updated_at": "2020-03-10T15:49:32Z", + "links": { + "self": { + "href": "https://api.example.org/v3/feature_flags/app_scaling" + } + } +} diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/featureflags/PATCH_{feature_flag_name}_request.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/featureflags/PATCH_{feature_flag_name}_request.json new file mode 100644 index 0000000000..6ca4291987 --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/featureflags/PATCH_{feature_flag_name}_request.json @@ -0,0 +1,4 @@ +{ + "enabled": false, + "custom_error_message": "This feature is disabled" +} diff --git a/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/featureflags/PATCH_{feature_flag_name}_response.json b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/featureflags/PATCH_{feature_flag_name}_response.json new file mode 100644 index 0000000000..4985c96c6f --- /dev/null +++ b/cloudfoundry-client-reactor/src/test/resources/fixtures/client/v3/featureflags/PATCH_{feature_flag_name}_response.json @@ -0,0 +1,11 @@ +{ + "name": "app_scaling", + "enabled": false, + "custom_error_message": "This feature is disabled", + "updated_at": "2020-03-10T15:49:32Z", + "links": { + "self": { + "href": "https://api.example.org/v3/feature_flags/app_scaling" + } + } +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java index 3d587dcfe3..61c0ac2bac 100644 --- a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2021 the original author or authors. + * Copyright 2013-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. @@ -55,6 +55,7 @@ import org.cloudfoundry.client.v3.deployments.DeploymentsV3; import org.cloudfoundry.client.v3.domains.DomainsV3; import org.cloudfoundry.client.v3.droplets.Droplets; +import org.cloudfoundry.client.v3.featureflags.FeatureFlagsV3; import org.cloudfoundry.client.v3.isolationsegments.IsolationSegments; import org.cloudfoundry.client.v3.jobs.JobsV3; import org.cloudfoundry.client.v3.organizations.OrganizationsV3; @@ -169,6 +170,11 @@ public interface CloudFoundryClient { */ FeatureFlags featureFlags(); + /** + * Main entry point to the Cloud Foundry Feature Flags V3 Client API + */ + FeatureFlagsV3 featureFlagsV3(); + /** * Main entry point to the Cloud Foundry Info Client API */ diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/FeatureFlagsV3.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/FeatureFlagsV3.java new file mode 100644 index 0000000000..f6a3a7f7c1 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/FeatureFlagsV3.java @@ -0,0 +1,50 @@ +/* + * 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.client.v3.featureflags; + +import reactor.core.publisher.Mono; + +/** + * Main entry point to the Cloud Foundry Feature Flags V3 Client API + * see: https://v3-apidocs.cloudfoundry.org/#feature-flags + */ +public interface FeatureFlagsV3 { + + /** + * Makes the Get a Feature Flag request + * + * @param request the Get Feature Flag request + * @return the response from the Get Feature Flag request + */ + Mono get(GetFeatureFlagRequest request); + + /** + * Makes the List Feature Flags request + * + * @param request the List Feature Flags request + * @return the response from the List Feature Flags request + */ + Mono list(ListFeatureFlagsRequest request); + + /** + * Makes the Update a Feature Flag request + * + * @param request the Update Feature Flag request + * @return the response from the Update Feature Flag request + */ + Mono update(UpdateFeatureFlagRequest request); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_GetFeatureFlagRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_GetFeatureFlagRequest.java new file mode 100644 index 0000000000..bb90e40b49 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_GetFeatureFlagRequest.java @@ -0,0 +1,35 @@ +/* + * 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.client.v3.featureflags; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.immutables.value.Value; + +/** + * The request payload for the Get Feature Flag operation + * + * see https://v3-apidocs.cloudfoundry.org/#get-a-feature-flag + */ +@Value.Immutable +abstract class _GetFeatureFlagRequest { + + /** + * The name of the feature flag + */ + @JsonIgnore + abstract String getName(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_GetFeatureFlagResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_GetFeatureFlagResponse.java new file mode 100644 index 0000000000..c3d97a343c --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_GetFeatureFlagResponse.java @@ -0,0 +1,67 @@ +/* + * 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.client.v3.featureflags; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.Map; +import org.cloudfoundry.AllowNulls; +import org.cloudfoundry.Nullable; +import org.cloudfoundry.client.v3.Link; +import org.immutables.value.Value; + +/** + * The response payload for the Get Feature Flag operation + */ +@JsonDeserialize +@Value.Immutable +abstract class _GetFeatureFlagResponse { + + /** + * The name of the feature flag + */ + @JsonProperty("name") + abstract String getName(); + + /** + * Whether the feature flag is enabled + */ + @JsonProperty("enabled") + abstract Boolean getEnabled(); + + /** + * The custom error message displayed to users when the flag is disabled + */ + @JsonProperty("custom_error_message") + @Nullable + abstract String getCustomErrorMessage(); + + /** + * The time the feature flag was last updated + */ + @JsonProperty("updated_at") + @Nullable + abstract String getUpdatedAt(); + + /** + * Links to related resources and actions for the feature flag + */ + @AllowNulls + @JsonProperty("links") + @Nullable + abstract Map getLinks(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_ListFeatureFlagsRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_ListFeatureFlagsRequest.java new file mode 100644 index 0000000000..0c148fcab4 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_ListFeatureFlagsRequest.java @@ -0,0 +1,28 @@ +/* + * 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.client.v3.featureflags; + +import org.cloudfoundry.client.v3.PaginatedRequest; +import org.immutables.value.Value; + +/** + * The request payload for the List Feature Flags operation + * + * see https://v3-apidocs.cloudfoundry.org/#list-feature-flags + */ +@Value.Immutable +abstract class _ListFeatureFlagsRequest extends PaginatedRequest {} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_ListFeatureFlagsResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_ListFeatureFlagsResponse.java new file mode 100644 index 0000000000..048dcf3ab8 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_ListFeatureFlagsResponse.java @@ -0,0 +1,30 @@ +/* + * 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.client.v3.featureflags; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.cloudfoundry.client.v3.PaginatedResponse; +import org.immutables.value.Value; + +/** + * The response payload for the List Feature Flags operation + * + * see https://v3-apidocs.cloudfoundry.org/#list-feature-flags + */ +@JsonDeserialize +@Value.Immutable +abstract class _ListFeatureFlagsResponse extends PaginatedResponse {} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_UpdateFeatureFlagRequest.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_UpdateFeatureFlagRequest.java new file mode 100644 index 0000000000..dddec0fc8b --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_UpdateFeatureFlagRequest.java @@ -0,0 +1,53 @@ +/* + * 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.client.v3.featureflags; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.Nullable; +import org.immutables.value.Value; + +/** + * The request payload for the Update Feature Flag operation + * + * see https://v3-apidocs.cloudfoundry.org/#update-a-feature-flag + */ +@JsonSerialize +@Value.Immutable +abstract class _UpdateFeatureFlagRequest { + + /** + * The name of the feature flag + */ + @JsonIgnore + abstract String getName(); + + /** + * Whether the feature flag is enabled + */ + @JsonProperty("enabled") + @Nullable + abstract Boolean getEnabled(); + + /** + * The custom error message displayed to users when the flag is disabled + */ + @JsonProperty("custom_error_message") + @Nullable + abstract String getCustomErrorMessage(); +} diff --git a/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_UpdateFeatureFlagResponse.java b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_UpdateFeatureFlagResponse.java new file mode 100644 index 0000000000..3ac12e07b9 --- /dev/null +++ b/cloudfoundry-client/src/main/java/org/cloudfoundry/client/v3/featureflags/_UpdateFeatureFlagResponse.java @@ -0,0 +1,69 @@ +/* + * 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.client.v3.featureflags; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.Map; +import org.cloudfoundry.AllowNulls; +import org.cloudfoundry.Nullable; +import org.cloudfoundry.client.v3.Link; +import org.immutables.value.Value; + +/** + * The response payload for the Update Feature Flag operation + * + * see https://v3-apidocs.cloudfoundry.org/#update-a-feature-flag + */ +@JsonDeserialize +@Value.Immutable +abstract class _UpdateFeatureFlagResponse { + + /** + * The name of the feature flag + */ + @JsonProperty("name") + abstract String getName(); + + /** + * Whether the feature flag is enabled + */ + @JsonProperty("enabled") + abstract Boolean getEnabled(); + + /** + * The custom error message displayed to users when the flag is disabled + */ + @JsonProperty("custom_error_message") + @Nullable + abstract String getCustomErrorMessage(); + + /** + * The time the feature flag was last updated + */ + @JsonProperty("updated_at") + @Nullable + abstract String getUpdatedAt(); + + /** + * Links to related resources and actions for the feature flag + */ + @AllowNulls + @JsonProperty("links") + @Nullable + abstract Map getLinks(); +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/featureflags/GetFeatureFlagRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/featureflags/GetFeatureFlagRequestTest.java new file mode 100644 index 0000000000..4a8f7df210 --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/featureflags/GetFeatureFlagRequestTest.java @@ -0,0 +1,34 @@ +/* + * 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.client.v3.featureflags; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +final class GetFeatureFlagRequestTest { + + @Test + void noName() { + assertThrows(IllegalStateException.class, () -> GetFeatureFlagRequest.builder().build()); + } + + @Test + void valid() { + GetFeatureFlagRequest.builder().name("app_scaling").build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/featureflags/ListFeatureFlagsRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/featureflags/ListFeatureFlagsRequestTest.java new file mode 100644 index 0000000000..7de69422ab --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/featureflags/ListFeatureFlagsRequestTest.java @@ -0,0 +1,48 @@ +/* + * 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.client.v3.featureflags; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +final class ListFeatureFlagsRequestTest { + + @Test + void invalidPage() { + assertThrows( + IllegalStateException.class, + () -> ListFeatureFlagsRequest.builder().page(0).build()); + } + + @Test + void invalidPerPage() { + assertThrows( + IllegalStateException.class, + () -> ListFeatureFlagsRequest.builder().perPage(0).build()); + } + + @Test + void valid() { + ListFeatureFlagsRequest.builder().build(); + } + + @Test + void validWithPagination() { + ListFeatureFlagsRequest.builder().page(1).perPage(50).orderBy("name").build(); + } +} diff --git a/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/featureflags/UpdateFeatureFlagRequestTest.java b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/featureflags/UpdateFeatureFlagRequestTest.java new file mode 100644 index 0000000000..d87b0946e0 --- /dev/null +++ b/cloudfoundry-client/src/test/java/org/cloudfoundry/client/v3/featureflags/UpdateFeatureFlagRequestTest.java @@ -0,0 +1,50 @@ +/* + * 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.client.v3.featureflags; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +final class UpdateFeatureFlagRequestTest { + + @Test + void noName() { + assertThrows( + IllegalStateException.class, + () -> UpdateFeatureFlagRequest.builder().enabled(true).build()); + } + + @Test + void valid() { + UpdateFeatureFlagRequest.builder().name("app_scaling").enabled(true).build(); + } + + @Test + void validWithCustomErrorMessage() { + UpdateFeatureFlagRequest.builder() + .name("app_scaling") + .enabled(false) + .customErrorMessage("This feature is disabled") + .build(); + } + + @Test + void validNoFields() { + UpdateFeatureFlagRequest.builder().name("app_scaling").build(); + } +} diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v2/SpacesTest.java b/integration-test/src/test/java/org/cloudfoundry/client/v2/SpacesTest.java index e729b3d12a..4561f496e3 100644 --- a/integration-test/src/test/java/org/cloudfoundry/client/v2/SpacesTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/client/v2/SpacesTest.java @@ -1888,7 +1888,8 @@ public void listServicesFilterByLabel() { @Test public void listServicesFilterByServiceBrokerId() { - List expectedValues = Arrays.asList(this.serviceName, this.serviceName + "-shareable"); + List expectedValues = + Arrays.asList(this.serviceName, this.serviceName + "-shareable"); Mono.zip(this.serviceBrokerId, this.spaceId) .flatMapMany( function( diff --git a/integration-test/src/test/java/org/cloudfoundry/client/v3/FeatureFlagsV3Test.java b/integration-test/src/test/java/org/cloudfoundry/client/v3/FeatureFlagsV3Test.java new file mode 100644 index 0000000000..a61d11bce7 --- /dev/null +++ b/integration-test/src/test/java/org/cloudfoundry/client/v3/FeatureFlagsV3Test.java @@ -0,0 +1,116 @@ +/* + * 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.client.v3; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.Arrays; +import java.util.stream.Collectors; +import org.cloudfoundry.AbstractIntegrationTest; +import org.cloudfoundry.client.CloudFoundryClient; +import org.cloudfoundry.client.v3.featureflags.GetFeatureFlagRequest; +import org.cloudfoundry.client.v3.featureflags.GetFeatureFlagResponse; +import org.cloudfoundry.client.v3.featureflags.ListFeatureFlagsRequest; +import org.cloudfoundry.client.v3.featureflags.UpdateFeatureFlagRequest; +import org.cloudfoundry.util.PaginationUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import reactor.test.StepVerifier; + +public class FeatureFlagsV3Test extends AbstractIntegrationTest { + + private static final String FLAG_NAME = "app_scaling"; + + @Autowired private CloudFoundryClient cloudFoundryClient; + + @Test + public void get() { + this.cloudFoundryClient + .featureFlagsV3() + .get(GetFeatureFlagRequest.builder().name(FLAG_NAME).build()) + .map(GetFeatureFlagResponse::getName) + .as(StepVerifier::create) + .expectNext(FLAG_NAME) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + @Test + public void list() { + PaginationUtils.requestClientV3Resources( + page -> + this.cloudFoundryClient + .featureFlagsV3() + .list(ListFeatureFlagsRequest.builder().page(page).build())) + .map(GetFeatureFlagResponse::getName) + .collect(Collectors.toSet()) + .as(StepVerifier::create) + .consumeNextWith( + names -> + assertThat(names) + .containsAll( + Arrays.asList( + "app_bits_upload", + "app_scaling", + "diego_docker", + "private_domain_creation", + "route_creation", + "service_instance_creation", + "set_roles_by_username", + "unset_roles_by_username", + "user_org_creation"))) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + + @Test + public void update() { + this.cloudFoundryClient + .featureFlagsV3() + .get(GetFeatureFlagRequest.builder().name(FLAG_NAME).build()) + .flatMap( + original -> + this.cloudFoundryClient + .featureFlagsV3() + .update( + UpdateFeatureFlagRequest.builder() + .name(FLAG_NAME) + .enabled(!original.getEnabled()) + .build()) + .flatMap( + toggled -> + this.cloudFoundryClient + .featureFlagsV3() + .update( + UpdateFeatureFlagRequest + .builder() + .name(FLAG_NAME) + .enabled( + original + .getEnabled()) + .build()) + .thenReturn( + toggled.getEnabled() + != original + .getEnabled()))) + .as(StepVerifier::create) + .expectNext(true) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } +}