From f51b3f15ab23d6702afd9c756d96f4132c3b8550 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Mon, 24 Aug 2026 14:18:27 +0200 Subject: [PATCH] feat(provider-tck): adopt the conformance suite in the OFREP provider OFREP is a protocol, not a vendor, so the suite needs no new infrastructure: flagd already serves the OFREP HTTP API on 8016 inside the flagd-testbed image that the flagd TCK suites use, alongside the launchpad control API on 8080. The Compose stack is therefore the same image with a different port exposed, and the whole adoption is one test class plus one dependency. Four capabilities are withheld, all traceable to the same fact: OfrepProvider implements FeatureProvider rather than extending EventProvider and overrides no lifecycle method, so it has no state, no stream, no poll loop and no initialize(). It cannot emit events (EVENTS), cannot observe the backend going away (STALE) or changing (CONFIGURATION_CHANGE), and cannot fail initialisation against a dead port (UNAVAILABLE_INIT). Each omission is justified against specific lines of the provider in the capabilities() javadoc. events.feature and lifecycle.feature are both tagged @events at feature level, so 5 scenarios are reported as skipped and 24 run. OBJECT and STRICT_NUMERIC_TYPING are both declared. Unlike the flagd provider, OFREP does not silently narrow a float to an integer: values are deserialised by a plain Jackson ObjectMapper into an untyped Object, so a JSON fraction arrives as Double and a JSON integer as Integer, and handleResolved admits a value only on an exact type.isInstance check. float-flag requested as an integer is reported as TYPE_MISMATCH with the code default rather than truncated to 0. Signed-off-by: Simon Schrottner --- providers/ofrep/pom.xml | 17 +++ .../providers/ofrep/e2e/OfrepTckTest.java | 125 ++++++++++++++++++ .../test/resources/tck/docker-compose.yaml | 17 +++ 3 files changed, 159 insertions(+) create mode 100644 providers/ofrep/src/test/java/dev/openfeature/contrib/providers/ofrep/e2e/OfrepTckTest.java create mode 100644 providers/ofrep/src/test/resources/tck/docker-compose.yaml diff --git a/providers/ofrep/pom.xml b/providers/ofrep/pom.xml index 3f2fd6695..66064d472 100644 --- a/providers/ofrep/pom.xml +++ b/providers/ofrep/pom.xml @@ -17,6 +17,11 @@ OFREP Provider https://openfeature.dev + + + [0.0.1,) + + 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