Rahul-Baradol
@@ -81,5 +86,17 @@
4.12.0
test
+
+
+
+ dev.openfeature.contrib.tools
+ provider-tck
+ ${provider-tck.version}
+ test
+
diff --git a/providers/ofrep/src/test/java/dev/openfeature/contrib/providers/ofrep/e2e/OfrepTckTest.java b/providers/ofrep/src/test/java/dev/openfeature/contrib/providers/ofrep/e2e/OfrepTckTest.java
new file mode 100644
index 000000000..b4153c240
--- /dev/null
+++ b/providers/ofrep/src/test/java/dev/openfeature/contrib/providers/ofrep/e2e/OfrepTckTest.java
@@ -0,0 +1,125 @@
+package dev.openfeature.contrib.providers.ofrep.e2e;
+
+import dev.openfeature.contrib.providers.ofrep.OfrepProvider;
+import dev.openfeature.contrib.providers.ofrep.OfrepProviderOptions;
+import dev.openfeature.contrib.tools.providertck.AbstractProviderTckTest;
+import dev.openfeature.contrib.tools.providertck.BackendEndpoint;
+import dev.openfeature.contrib.tools.providertck.Capability;
+import dev.openfeature.sdk.FeatureProvider;
+import java.io.File;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Runs the OpenFeature Provider TCK against the OFREP provider.
+ *
+ * OFREP is a protocol rather than a vendor, so the backend under test is simply something that
+ * speaks it. flagd does, on port {@value #OFREP_PORT}, which means this suite reuses the flagd
+ * testbed image and its launchpad control API unchanged — see
+ * {@code src/test/resources/tck/docker-compose.yaml}.
+ */
+public class OfrepTckTest extends AbstractProviderTckTest {
+
+ /** The container-internal port flagd serves the OFREP HTTP API on. */
+ private static final int OFREP_PORT = 8016;
+
+ /**
+ * A port nothing listens on, for the initialisation-failure scenarios.
+ *
+ *
Deliberately not a port on the Compose stack: the stack must stay up for the whole suite,
+ * and simulated outages belong to the control API.
+ */
+ private static final int UNAVAILABLE_PORT = 9999;
+
+ @Override
+ public File composeFile() {
+ return new File("src/test/resources/tck/docker-compose.yaml");
+ }
+
+ @Override
+ public List backendPorts() {
+ return Collections.singletonList(OFREP_PORT);
+ }
+
+ @Override
+ public FeatureProvider createProvider(BackendEndpoint endpoint) {
+ return OfrepProvider.constructProvider(OfrepProviderOptions.builder()
+ .baseUrl("http://" + endpoint.host() + ":" + endpoint.port(OFREP_PORT))
+ .build());
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Short timeouts on purpose: the {@code @unavailable} scenarios assert that failure is
+ * reported promptly. They are skipped for this provider — see
+ * {@link #capabilities()} — but the deadlines stay correct so that the scenarios start passing
+ * on their own the day the provider grows an {@code initialize()}.
+ */
+ @Override
+ public FeatureProvider createUnavailableProvider() {
+ return OfrepProvider.constructProvider(OfrepProviderOptions.builder()
+ .baseUrl("http://localhost:" + UNAVAILABLE_PORT)
+ .connectTimeout(Duration.ofMillis(500))
+ .requestTimeout(Duration.ofMillis(500))
+ .build());
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ *
Four capabilities are withheld, and all four come from the same root cause: {@code
+ * OfrepProvider} is a bare {@link dev.openfeature.sdk.FeatureProvider} (OfrepProvider.java:19)
+ * with no lifecycle of its own. It holds no state, opens no stream, runs no poll loop and does
+ * not override {@code initialize()} — every evaluation is a fresh, independent HTTP POST
+ * (Resolver.java:54-97, OfrepApi.java:93-118). There is nothing in it that could observe a
+ * backend transition, let alone report one.
+ *
+ *
+ * - {@link Capability#EVENTS} — the class declares {@code implements FeatureProvider},
+ * not {@code extends EventProvider} (OfrepProvider.java:19), so it has no {@code emit*}
+ * method available and calls none. The whole file contains no reference to
+ * {@code ProviderEvent}. The {@code PROVIDER_READY} that a client does observe is
+ * synthesised by the SDK on successful initialisation and would appear for
+ * {@code NoOpProvider} just the same; it is not the provider participating in the event
+ * system, so declaring the capability would be claiming behaviour the provider does not
+ * have — and would silently assert an untestable {@code PROVIDER_ERROR}.
+ *
- {@link Capability#STALE} — requires noticing that the backend went away between
+ * evaluations. Nothing survives a call: {@code Resolver.resolve} builds its result purely
+ * from the current response and, on {@code IOException}, returns
+ * {@code ErrorCode.GENERAL} without recording anything (Resolver.java:93-96,
+ * OfrepApi.java:114-115). No state, no transition, no {@code PROVIDER_STALE}.
+ *
- {@link Capability#CONFIGURATION_CHANGE} — needs a subscription to the backend.
+ * The only outbound call in the provider is the per-evaluation
+ * {@code POST /ofrep/v1/evaluate/flags/{key}} (OfrepApi.java:27, 93-109). There is no
+ * bulk endpoint, no ETag handling and no watch, so a change is never detected as an
+ * event — merely reflected by the next evaluation.
+ *
- {@link Capability#UNAVAILABLE_INIT} — {@code OfrepProvider} does not override
+ * {@code initialize(EvaluationContext)}, so the interface default runs and initialisation
+ * cannot fail. {@code constructProvider} only validates its arguments; it never touches
+ * the network (OfrepProvider.java:38-68). A provider pointed at a dead port therefore
+ * reaches {@code READY}, which is the opposite of what the scenario asserts.
+ *
+ *
+ * {@link Capability#OBJECT} and {@link Capability#STRICT_NUMERIC_TYPING} are both declared,
+ * and the second is worth spelling out because the flagd provider cannot declare it.
+ * Deserialisation goes through a plain Jackson {@code ObjectMapper} into an untyped
+ * {@code Object value} (OfrepResponse.java:16, OfrepApi.java:109), which maps a JSON integer to
+ * {@link Integer} and a JSON fraction to {@link Double}. {@code handleResolved} then admits the
+ * value only on an exact {@code type.isInstance(responseValue)} check and otherwise returns
+ * {@code TYPE_MISMATCH} with the code default (Resolver.java:183-190). Nothing anywhere widens
+ * or narrows between the two numeric types, so {@code float-flag} (0.5) requested as an integer
+ * is rejected rather than truncated to {@code 0}. The same exact-instance check is what makes
+ * the {@code @object} mismatch matrix work; the structured happy path passes through
+ * {@code resolve(Object.class, ...)}, which every non-null value satisfies, and is converted
+ * with {@code Value.objectToValue} (Resolver.java:125-136).
+ */
+ @Override
+ public Set capabilities() {
+ return EnumSet.complementOf(EnumSet.of(
+ Capability.EVENTS, Capability.STALE, Capability.CONFIGURATION_CHANGE, Capability.UNAVAILABLE_INIT));
+ }
+}
diff --git a/providers/ofrep/src/test/resources/tck/docker-compose.yaml b/providers/ofrep/src/test/resources/tck/docker-compose.yaml
new file mode 100644
index 000000000..93aa4cf80
--- /dev/null
+++ b/providers/ofrep/src/test/resources/tck/docker-compose.yaml
@@ -0,0 +1,17 @@
+# Backend stack for the OpenFeature Provider TCK, wrapping the unmodified flagd testbed image.
+#
+# OFREP is a vendor-neutral protocol, so the TCK does not need an OFREP-specific testbed: any
+# backend that speaks OFREP and exposes the control API will do. flagd serves the OFREP HTTP API
+# on 8016 alongside its own gRPC surfaces, and the testbed image already ships the "launchpad"
+# control API on 8080 — the same stack the flagd provider's TCK suite uses, seeded with the same
+# canonical flag set. No new image, no new control API.
+#
+# Note there are no host port bindings. The TCK requires dynamically mapped ports and discovers
+# them after startup — a pinned host port would make the suite unrunnable in parallel and would
+# collide with a developer's local flagd.
+services:
+ backend:
+ image: ghcr.io/open-feature/flagd-testbed:v3.8.0
+ ports:
+ - 8016 # flagd OFREP evaluation (HTTP)
+ - 8080 # launchpad control API