cached = cache.get(secretName);
- if (cached.isPresent()) {
- return cached.get();
- }
- synchronized (this) {
- return cache.get(secretName).orElseGet(() -> {
- String value = fetchFromGcp(secretName);
- cache.put(secretName, value);
- return value;
- });
- }
- }
-
- /**
- * Applies the configured prefix (if any) and returns the GCP secret name for the flag.
- */
- private String buildSecretName(String flagKey) {
- String prefix = options.getNamePrefix();
- return (prefix != null && !prefix.isEmpty()) ? prefix + flagKey : flagKey;
- }
-
- /**
- * Accesses the configured version of the named secret from GCP Secret Manager.
- *
- * @param secretName the GCP secret name (without project path)
- * @return the UTF-8 string value of the secret payload
- * @throws FlagNotFoundError when the secret does not exist
- * @throws GeneralError for any other GCP API error
- */
- private String fetchFromGcp(String secretName) {
+ protected String fetchFromGcp(String secretName) {
try {
SecretVersionName versionName =
SecretVersionName.of(options.getProjectId(), secretName, options.getVersion());
diff --git a/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java
new file mode 100644
index 000000000..9a0b0f3da
--- /dev/null
+++ b/providers/gcp/src/main/java/dev/openfeature/contrib/providers/gcp/ParameterManagerClientFactory.java
@@ -0,0 +1,34 @@
+package dev.openfeature.contrib.providers.gcp;
+
+import com.google.api.gax.core.FixedCredentialsProvider;
+import com.google.cloud.parametermanager.v1.ParameterManagerClient;
+import com.google.cloud.parametermanager.v1.ParameterManagerSettings;
+import java.io.IOException;
+
+/**
+ * Factory for creating a {@link ParameterManagerClient}, separated to allow injection
+ * of mock clients in unit tests.
+ */
+final class ParameterManagerClientFactory {
+
+ private ParameterManagerClientFactory() {}
+
+ /**
+ * Creates a new {@link ParameterManagerClient} using the provided options.
+ *
+ * When {@link GcpProviderOptions#getCredentials()} is non-null, those
+ * credentials are used explicitly. Otherwise, the GCP client library falls back to
+ * Application Default Credentials (ADC) automatically.
+ *
+ * @param options the provider options
+ * @return a configured {@link ParameterManagerClient}
+ * @throws IOException if the client cannot be created
+ */
+ static ParameterManagerClient create(GcpProviderOptions options) throws IOException {
+ ParameterManagerSettings.Builder settingsBuilder = ParameterManagerSettings.newBuilder();
+ if (options.getCredentials() != null) {
+ settingsBuilder.setCredentialsProvider(FixedCredentialsProvider.create(options.getCredentials()));
+ }
+ return ParameterManagerClient.create(settingsBuilder.build());
+ }
+}
diff --git a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProviderTest.java b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProviderTest.java
new file mode 100644
index 000000000..0691e1b41
--- /dev/null
+++ b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/AbstractGcpProviderTest.java
@@ -0,0 +1,245 @@
+package dev.openfeature.contrib.providers.gcp;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.ImmutableContext;
+import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.Reason;
+import dev.openfeature.sdk.Value;
+import dev.openfeature.sdk.exceptions.FlagNotFoundError;
+import dev.openfeature.sdk.exceptions.GeneralError;
+import dev.openfeature.sdk.exceptions.ParseError;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+@DisplayName("GCP provider shared behavior")
+abstract class AbstractGcpProviderTest {
+
+ protected GcpProviderOptions options;
+ protected FeatureProvider provider;
+
+ protected abstract FeatureProvider createProvider(GcpProviderOptions options);
+
+ protected abstract FeatureProvider createProvider(GcpProviderOptions options, Object client);
+
+ protected abstract void stubFetchSuccess(String value);
+
+ protected abstract void stubFetchNotFound();
+
+ protected abstract void stubFetchError(String message);
+
+ protected abstract void verifyFetchCalled(int times);
+
+ protected abstract void verifyClientClosed(int times);
+
+ protected abstract String getProviderName();
+
+ protected abstract GcpProviderOptions.GcpProviderOptionsBuilder newOptionsBuilder();
+
+ @BeforeEach
+ void setUp() throws Exception {
+ options = newOptionsBuilder().build();
+ provider = createProvider(options);
+ provider.initialize(new ImmutableContext());
+ }
+
+ @Nested
+ @DisplayName("Metadata")
+ class MetadataTests {
+
+ @Test
+ @DisplayName("returns the correct provider name")
+ void providerName() {
+ assertThat(provider.getMetadata().getName()).isEqualTo(getProviderName());
+ }
+ }
+
+ @Nested
+ @DisplayName("Initialization")
+ class InitializationTests {
+
+ @Test
+ @DisplayName("throws IllegalArgumentException when projectId is blank")
+ void blankProjectIdThrows() {
+ GcpProviderOptions badOpts = newOptionsBuilder().projectId("").build();
+ FeatureProvider badProvider = createProvider(badOpts);
+ assertThatThrownBy(() -> badProvider.initialize(new ImmutableContext()))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ @DisplayName("throws IllegalArgumentException when projectId is null")
+ void nullProjectIdThrows() {
+ GcpProviderOptions badOpts = newOptionsBuilder().projectId(null).build();
+ FeatureProvider badProvider = createProvider(badOpts);
+ assertThatThrownBy(() -> badProvider.initialize(new ImmutableContext()))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+ }
+
+ @Nested
+ @DisplayName("Boolean evaluation")
+ class BooleanEvaluation {
+
+ @Test
+ @DisplayName("returns true for value 'true'")
+ void trueValue() {
+ stubFetchSuccess("true");
+ ProviderEvaluation result =
+ provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext());
+ assertThat(result.getValue()).isTrue();
+ assertThat(result.getReason()).isEqualTo(Reason.STATIC.toString());
+ }
+
+ @Test
+ @DisplayName("returns false for value 'false'")
+ void falseValue() {
+ stubFetchSuccess("false");
+ ProviderEvaluation result =
+ provider.getBooleanEvaluation("bool-flag", true, new ImmutableContext());
+ assertThat(result.getValue()).isFalse();
+ }
+
+ @Test
+ @DisplayName("throws ParseError for malformed boolean value")
+ void malformedBooleanThrows() {
+ stubFetchSuccess("not-a-bool");
+ assertThatThrownBy(() -> provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext()))
+ .isInstanceOf(ParseError.class);
+ }
+ }
+
+ @Nested
+ @DisplayName("String evaluation")
+ class StringEvaluation {
+
+ @Test
+ @DisplayName("returns string value as-is")
+ void stringValue() {
+ stubFetchSuccess("dark-mode");
+ ProviderEvaluation result =
+ provider.getStringEvaluation("str-flag", "light-mode", new ImmutableContext());
+ assertThat(result.getValue()).isEqualTo("dark-mode");
+ }
+ }
+
+ @Nested
+ @DisplayName("Integer evaluation")
+ class IntegerEvaluation {
+
+ @Test
+ @DisplayName("parses numeric string to Integer")
+ void integerValue() {
+ stubFetchSuccess("42");
+ ProviderEvaluation result = provider.getIntegerEvaluation("int-flag", 0, new ImmutableContext());
+ assertThat(result.getValue()).isEqualTo(42);
+ }
+
+ @Test
+ @DisplayName("throws ParseError for non-numeric value")
+ void nonNumericThrows() {
+ stubFetchSuccess("abc");
+ assertThatThrownBy(() -> provider.getIntegerEvaluation("int-flag", 0, new ImmutableContext()))
+ .isInstanceOf(ParseError.class);
+ }
+ }
+
+ @Nested
+ @DisplayName("Double evaluation")
+ class DoubleEvaluation {
+
+ @Test
+ @DisplayName("parses numeric string to Double")
+ void doubleValue() {
+ stubFetchSuccess("3.14");
+ ProviderEvaluation result =
+ provider.getDoubleEvaluation("double-flag", 0.0, new ImmutableContext());
+ assertThat(result.getValue()).isEqualTo(3.14);
+ }
+ }
+
+ @Nested
+ @DisplayName("Object evaluation")
+ class ObjectEvaluation {
+
+ @Test
+ @DisplayName("parses JSON string to Value/Structure")
+ void jsonValue() {
+ stubFetchSuccess("{\"color\":\"blue\",\"count\":3}");
+ ProviderEvaluation result =
+ provider.getObjectEvaluation("obj-flag", new Value(), new ImmutableContext());
+ assertThat(result.getValue().asStructure()).isNotNull();
+ assertThat(result.getValue().asStructure().getValue("color").asString())
+ .isEqualTo("blue");
+ }
+ }
+
+ @Nested
+ @DisplayName("Error handling")
+ class ErrorHandling {
+
+ @Test
+ @DisplayName("throws FlagNotFoundError when value does not exist")
+ void flagNotFound() {
+ stubFetchNotFound();
+ assertThatThrownBy(() -> provider.getBooleanEvaluation("missing-flag", false, new ImmutableContext()))
+ .isInstanceOf(FlagNotFoundError.class);
+ }
+
+ @Test
+ @DisplayName("throws GeneralError on unexpected API exception")
+ void apiError() {
+ stubFetchError("Connection refused");
+ assertThatThrownBy(() -> provider.getBooleanEvaluation("flag", false, new ImmutableContext()))
+ .isInstanceOf(GeneralError.class);
+ }
+ }
+
+ @Nested
+ @DisplayName("Caching")
+ class CachingTests {
+
+ @Test
+ @DisplayName("cache hit: API called only once for repeated evaluations")
+ void cacheHit() {
+ stubFetchSuccess("true");
+ provider.getBooleanEvaluation("cached-flag", false, new ImmutableContext());
+ provider.getBooleanEvaluation("cached-flag", false, new ImmutableContext());
+ verifyFetchCalled(1);
+ }
+ }
+
+ @Nested
+ @DisplayName("Name prefix")
+ class PrefixTests {
+
+ @Test
+ @DisplayName("prefix is prepended to the flag key when building the resource name")
+ void prefixApplied() throws Exception {
+ GcpProviderOptions prefixedOpts =
+ newOptionsBuilder().namePrefix("ff-").build();
+ FeatureProvider prefixedProvider = createProvider(prefixedOpts);
+ stubFetchSuccess("true");
+ prefixedProvider.initialize(new ImmutableContext());
+ ProviderEvaluation result =
+ prefixedProvider.getBooleanEvaluation("my-flag", false, new ImmutableContext());
+ assertThat(result.getValue()).isTrue();
+ }
+ }
+
+ @Nested
+ @DisplayName("Lifecycle")
+ class LifecycleTests {
+
+ @Test
+ @DisplayName("shutdown() closes the client")
+ void shutdownClosesClient() {
+ provider.shutdown();
+ verifyClientClosed(1);
+ }
+ }
+}
diff --git a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java
new file mode 100644
index 000000000..4716fcc02
--- /dev/null
+++ b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpParameterManagerProviderTest.java
@@ -0,0 +1,74 @@
+package dev.openfeature.contrib.providers.gcp;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.gax.rpc.NotFoundException;
+import com.google.cloud.parametermanager.v1.ParameterManagerClient;
+import com.google.cloud.parametermanager.v1.ParameterVersionName;
+import com.google.cloud.parametermanager.v1.RenderParameterVersionResponse;
+import com.google.protobuf.ByteString;
+import dev.openfeature.sdk.FeatureProvider;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@DisplayName("GcpParameterManagerProvider")
+@ExtendWith(MockitoExtension.class)
+class GcpParameterManagerProviderTest extends AbstractGcpProviderTest {
+
+ @Mock
+ private ParameterManagerClient mockClient;
+
+ @Override
+ protected FeatureProvider createProvider(GcpProviderOptions options) {
+ return new GcpParameterManagerProvider(options, mockClient);
+ }
+
+ @Override
+ protected FeatureProvider createProvider(GcpProviderOptions options, Object client) {
+ return new GcpParameterManagerProvider(options, (ParameterManagerClient) client);
+ }
+
+ @Override
+ protected void stubFetchSuccess(String value) {
+ RenderParameterVersionResponse response = RenderParameterVersionResponse.newBuilder()
+ .setRenderedPayload(ByteString.copyFromUtf8(value))
+ .build();
+ when(mockClient.renderParameterVersion(any(ParameterVersionName.class))).thenReturn(response);
+ }
+
+ @Override
+ protected void stubFetchNotFound() {
+ when(mockClient.renderParameterVersion(any(ParameterVersionName.class))).thenThrow(NotFoundException.class);
+ }
+
+ @Override
+ protected void stubFetchError(String message) {
+ when(mockClient.renderParameterVersion(any(ParameterVersionName.class)))
+ .thenThrow(new RuntimeException(message));
+ }
+
+ @Override
+ protected void verifyFetchCalled(int times) {
+ verify(mockClient, times(times)).renderParameterVersion(any(ParameterVersionName.class));
+ }
+
+ @Override
+ protected void verifyClientClosed(int times) {
+ verify(mockClient, times(times)).close();
+ }
+
+ @Override
+ protected String getProviderName() {
+ return GcpParameterManagerProvider.PROVIDER_NAME;
+ }
+
+ @Override
+ protected GcpProviderOptions.GcpProviderOptionsBuilder newOptionsBuilder() {
+ return GcpProviderOptions.builder().projectId("test-project").locationId("europe-west1");
+ }
+}
diff --git a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProviderTest.java b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProviderTest.java
index 5e6618ee3..b976883f9 100644
--- a/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProviderTest.java
+++ b/providers/gcp/src/test/java/dev/openfeature/contrib/providers/gcp/GcpSecretManagerProviderTest.java
@@ -1,7 +1,5 @@
package dev.openfeature.contrib.providers.gcp;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -13,39 +11,31 @@
import com.google.cloud.secretmanager.v1.SecretPayload;
import com.google.cloud.secretmanager.v1.SecretVersionName;
import com.google.protobuf.ByteString;
-import dev.openfeature.sdk.ImmutableContext;
-import dev.openfeature.sdk.ProviderEvaluation;
-import dev.openfeature.sdk.Reason;
-import dev.openfeature.sdk.Value;
-import dev.openfeature.sdk.exceptions.FlagNotFoundError;
-import dev.openfeature.sdk.exceptions.GeneralError;
-import dev.openfeature.sdk.exceptions.ParseError;
-import org.junit.jupiter.api.BeforeEach;
+import dev.openfeature.sdk.FeatureProvider;
import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@DisplayName("GcpSecretManagerProvider")
@ExtendWith(MockitoExtension.class)
-class GcpSecretManagerProviderTest {
+class GcpSecretManagerProviderTest extends AbstractGcpProviderTest {
@Mock
private SecretManagerServiceClient mockClient;
- private GcpProviderOptions options;
- private GcpSecretManagerProvider provider;
+ @Override
+ protected FeatureProvider createProvider(GcpProviderOptions options) {
+ return new GcpSecretManagerProvider(options, mockClient);
+ }
- @BeforeEach
- void setUp() throws Exception {
- options = GcpProviderOptions.builder().projectId("test-project").build();
- provider = new GcpSecretManagerProvider(options, mockClient);
- provider.initialize(new ImmutableContext());
+ @Override
+ protected FeatureProvider createProvider(GcpProviderOptions options, Object client) {
+ return new GcpSecretManagerProvider(options, (SecretManagerServiceClient) client);
}
- private void stubSecret(String value) {
+ @Override
+ protected void stubFetchSuccess(String value) {
AccessSecretVersionResponse response = AccessSecretVersionResponse.newBuilder()
.setPayload(SecretPayload.newBuilder()
.setData(ByteString.copyFromUtf8(value))
@@ -54,214 +44,33 @@ private void stubSecret(String value) {
when(mockClient.accessSecretVersion(any(SecretVersionName.class))).thenReturn(response);
}
- private void stubSecretNotFound() {
+ @Override
+ protected void stubFetchNotFound() {
when(mockClient.accessSecretVersion(any(SecretVersionName.class))).thenThrow(NotFoundException.class);
}
- private void stubSecretError(String message) {
+ @Override
+ protected void stubFetchError(String message) {
when(mockClient.accessSecretVersion(any(SecretVersionName.class))).thenThrow(new RuntimeException(message));
}
- @Nested
- @DisplayName("Metadata")
- class MetadataTests {
-
- @Test
- @DisplayName("returns the correct provider name")
- void providerName() {
- assertThat(provider.getMetadata().getName()).isEqualTo(GcpSecretManagerProvider.PROVIDER_NAME);
- }
- }
-
- @Nested
- @DisplayName("Initialization")
- class InitializationTests {
-
- @Test
- @DisplayName("throws IllegalArgumentException when projectId is blank")
- void blankProjectIdThrows() {
- GcpProviderOptions badOpts =
- GcpProviderOptions.builder().projectId("").build();
- GcpSecretManagerProvider badProvider = new GcpSecretManagerProvider(badOpts, mockClient);
- assertThatThrownBy(() -> badProvider.initialize(new ImmutableContext()))
- .isInstanceOf(IllegalArgumentException.class);
- }
-
- @Test
- @DisplayName("throws IllegalArgumentException when projectId is null")
- void nullProjectIdThrows() {
- GcpProviderOptions badOpts = GcpProviderOptions.builder().build();
- GcpSecretManagerProvider badProvider = new GcpSecretManagerProvider(badOpts, mockClient);
- assertThatThrownBy(() -> badProvider.initialize(new ImmutableContext()))
- .isInstanceOf(IllegalArgumentException.class);
- }
- }
-
- @Nested
- @DisplayName("Boolean evaluation")
- class BooleanEvaluation {
-
- @Test
- @DisplayName("returns true for secret value 'true'")
- void trueValue() {
- stubSecret("true");
- ProviderEvaluation result =
- provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext());
- assertThat(result.getValue()).isTrue();
- assertThat(result.getReason()).isEqualTo(Reason.STATIC.toString());
- }
-
- @Test
- @DisplayName("returns false for secret value 'false'")
- void falseValue() {
- stubSecret("false");
- ProviderEvaluation result =
- provider.getBooleanEvaluation("bool-flag", true, new ImmutableContext());
- assertThat(result.getValue()).isFalse();
- }
-
- @Test
- @DisplayName("throws ParseError for malformed boolean value")
- void malformedBooleanThrows() {
- stubSecret("not-a-bool");
- assertThatThrownBy(() -> provider.getBooleanEvaluation("bool-flag", false, new ImmutableContext()))
- .isInstanceOf(ParseError.class);
- }
+ @Override
+ protected void verifyFetchCalled(int times) {
+ verify(mockClient, times(times)).accessSecretVersion(any(SecretVersionName.class));
}
- @Nested
- @DisplayName("String evaluation")
- class StringEvaluation {
-
- @Test
- @DisplayName("returns string value as-is")
- void stringValue() {
- stubSecret("dark-mode");
- ProviderEvaluation result =
- provider.getStringEvaluation("str-flag", "light-mode", new ImmutableContext());
- assertThat(result.getValue()).isEqualTo("dark-mode");
- }
+ @Override
+ protected void verifyClientClosed(int times) {
+ verify(mockClient, times(times)).close();
}
- @Nested
- @DisplayName("Integer evaluation")
- class IntegerEvaluation {
-
- @Test
- @DisplayName("parses numeric string to Integer")
- void integerValue() {
- stubSecret("42");
- ProviderEvaluation result = provider.getIntegerEvaluation("int-flag", 0, new ImmutableContext());
- assertThat(result.getValue()).isEqualTo(42);
- }
-
- @Test
- @DisplayName("throws ParseError for non-numeric value")
- void nonNumericThrows() {
- stubSecret("abc");
- assertThatThrownBy(() -> provider.getIntegerEvaluation("int-flag", 0, new ImmutableContext()))
- .isInstanceOf(ParseError.class);
- }
+ @Override
+ protected String getProviderName() {
+ return GcpSecretManagerProvider.PROVIDER_NAME;
}
- @Nested
- @DisplayName("Double evaluation")
- class DoubleEvaluation {
-
- @Test
- @DisplayName("parses numeric string to Double")
- void doubleValue() {
- stubSecret("3.14");
- ProviderEvaluation result =
- provider.getDoubleEvaluation("double-flag", 0.0, new ImmutableContext());
- assertThat(result.getValue()).isEqualTo(3.14);
- }
- }
-
- @Nested
- @DisplayName("Object evaluation")
- class ObjectEvaluation {
-
- @Test
- @DisplayName("parses JSON string to Value/Structure")
- void jsonValue() {
- stubSecret("{\"color\":\"blue\",\"count\":3}");
- ProviderEvaluation result =
- provider.getObjectEvaluation("obj-flag", new Value(), new ImmutableContext());
- assertThat(result.getValue().asStructure()).isNotNull();
- assertThat(result.getValue().asStructure().getValue("color").asString())
- .isEqualTo("blue");
- }
- }
-
- @Nested
- @DisplayName("Error handling")
- class ErrorHandling {
-
- @Test
- @DisplayName("throws FlagNotFoundError when secret does not exist")
- void flagNotFound() {
- stubSecretNotFound();
- assertThatThrownBy(() -> provider.getBooleanEvaluation("missing-flag", false, new ImmutableContext()))
- .isInstanceOf(FlagNotFoundError.class);
- }
-
- @Test
- @DisplayName("throws GeneralError on unexpected GCP API exception")
- void gcpApiError() {
- stubSecretError("Connection refused");
- assertThatThrownBy(() -> provider.getBooleanEvaluation("flag", false, new ImmutableContext()))
- .isInstanceOf(GeneralError.class);
- }
- }
-
- @Nested
- @DisplayName("Caching")
- class CachingTests {
-
- @Test
- @DisplayName("cache hit: GCP client called only once for two consecutive evaluations")
- void cacheHit() {
- stubSecret("true");
- provider.getBooleanEvaluation("cached-flag", false, new ImmutableContext());
- provider.getBooleanEvaluation("cached-flag", false, new ImmutableContext());
- verify(mockClient, times(1)).accessSecretVersion(any(SecretVersionName.class));
- }
- }
-
- @Nested
- @DisplayName("Secret name prefix")
- class PrefixTests {
-
- @Test
- @DisplayName("prefix is prepended to the flag key when building secret name")
- void prefixApplied() {
- GcpProviderOptions prefixedOpts = GcpProviderOptions.builder()
- .projectId("test-project")
- .namePrefix("ff-")
- .build();
- stubSecret("true");
- GcpSecretManagerProvider prefixedProvider = new GcpSecretManagerProvider(prefixedOpts, mockClient);
- try {
- prefixedProvider.initialize(new ImmutableContext());
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- ProviderEvaluation result =
- prefixedProvider.getBooleanEvaluation("my-flag", false, new ImmutableContext());
- assertThat(result.getValue()).isTrue();
- }
- }
-
- @Nested
- @DisplayName("Lifecycle")
- class LifecycleTests {
-
- @Test
- @DisplayName("shutdown() closes the GCP client")
- void shutdownClosesClient() {
- provider.shutdown();
- verify(mockClient, times(1)).close();
- }
+ @Override
+ protected GcpProviderOptions.GcpProviderOptionsBuilder newOptionsBuilder() {
+ return GcpProviderOptions.builder().projectId("test-project");
}
}
diff --git a/providers/gcp/version.txt b/providers/gcp/version.txt
index 8acdd82b7..4e379d2bf 100644
--- a/providers/gcp/version.txt
+++ b/providers/gcp/version.txt
@@ -1 +1 @@
-0.0.1
+0.0.2
diff --git a/samples/gcp/README.md b/samples/gcp/README.md
index d27981e46..52426413a 100644
--- a/samples/gcp/README.md
+++ b/samples/gcp/README.md
@@ -1,9 +1,10 @@
# GCP — OpenFeature Sample
-A runnable Java application demonstrating the [GCP Secret Manager OpenFeature provider](../../providers/gcp).
+A runnable Java application demonstrating the [GCP Secret Manager](../../providers/gcp) and
+[GCP Parameter Manager](../../providers/gcp) OpenFeature providers.
-It evaluates five feature flags (covering every supported type) that are stored as secrets in
-Google Cloud Secret Manager.
+It evaluates five feature flags (covering every supported type) that are stored with the
+`of-sample-` prefix in either Google Cloud Secret Manager or Google Cloud Parameter Manager.
## Feature Flags Used
@@ -57,26 +58,30 @@ mvn install -DskipTests -P '!deploy'
This installs the provider JAR to your local Maven repository (`~/.m2`).
-## Step 4 — Create the feature-flag secrets
+## Step 4 — Create the feature-flag secrets or parameters
```bash
cd samples/gcp
-bash setup.sh
+bash setup.sh # Creates secrets (default: Secret Manager)
+# OR
+bash setup.sh parameter-manager # Creates parameters in Parameter Manager
```
You should see output like:
```
-Creating sample feature-flag secrets in project: my-gcp-project
+Creating sample feature-flag secret-manager in project: my-gcp-project
[CREATED] of-sample-dark-mode
[VERSION] of-sample-dark-mode → true
[CREATED] of-sample-banner-text
...
-✓ All secrets created successfully.
+✓ All secret-manager entries created successfully.
```
## Step 5 — Run the sample
+By default the module runs the Secret Manager sample:
+
```bash
mvn exec:java
```
@@ -87,6 +92,15 @@ The app reads `GCP_PROJECT_ID` from the environment. You can also pass it explic
mvn exec:java -DGCP_PROJECT_ID=my-gcp-project
```
+### Run the Parameter Manager sample
+
+```bash
+mvn exec:java -Dexec.mainClass=dev.openfeature.contrib.samples.gcp.ParameterManagerSampleApp
+```
+
+This uses the same sample flag names and prefix. If you want to evaluate with Parameter Manager,
+create the sample parameters in your project under `of-sample-`.
+
### Expected output
```
@@ -124,7 +138,9 @@ Express checkout : true
## Step 6 — Clean up
```bash
-bash teardown.sh
+bash teardown.sh # Deletes secrets (default)
+# OR
+bash teardown.sh parameter-manager # Deletes Parameter Manager parameters
```
---
@@ -147,8 +163,9 @@ Re-run the sample to see the new value (cache expires after 30 seconds in this s
| Error | Cause | Fix |
|---|---|---|
| `GCP_PROJECT_ID is not set` | Env var missing | `export GCP_PROJECT_ID=my-project` |
-| `FlagNotFoundError` | Secret doesn't exist | Run `setup.sh` first |
-| `PERMISSION_DENIED` | Missing IAM role | Grant `roles/secretmanager.secretAccessor` |
+| `FlagNotFoundError` | Secret/parameter doesn't exist | Run `setup.sh` first (or `setup.sh parameter-manager`) |
+| `PERMISSION_DENIED` | Missing IAM role | Grant `roles/secretmanager.secretAccessor` or `roles/secretmanager.parameterAccessor` |
| `UNAUTHENTICATED` | No credentials | Run `gcloud auth application-default login` |
| `secretmanager.googleapis.com is not enabled` | API disabled | Run Step 1 |
| `Could not find artifact ...gcp` | Provider not installed | Run Step 3 |
+| `Invalid choice: 'parameter-manager'` | gcloud alpha components missing | Run `gcloud components install alpha` |
diff --git a/samples/gcp/pom.xml b/samples/gcp/pom.xml
index 664ceef42..9a79bccfb 100644
--- a/samples/gcp/pom.xml
+++ b/samples/gcp/pom.xml
@@ -23,6 +23,7 @@
${java.version}
${java.version}
UTF-8
+ dev.openfeature.contrib.samples.gcp.SecretManagerSampleApp
${env.GCP_PROJECT_ID}