backendPorts() {
+ return Arrays.asList(PROXY_PORT);
+ }
+
+ @Override
+ public FeatureProvider createProvider(BackendEndpoint endpoint) {
+ // The Flagsmith SDK appends its own path segments, so baseUri is the API root with a
+ // trailing slash: remote evaluation requests "flags/" beneath it.
+ String baseUri = String.format(
+ "http://%s:%d/api/v1/", endpoint.host(), endpoint.port(PROXY_PORT));
+
+ FlagsmithProviderOptions options = FlagsmithProviderOptions.builder()
+ .apiKey(SERVER_SIDE_KEY)
+ .baseUri(baseUri)
+ // usingBooleanConfigValue is the setting the three languages disagree about.
+ //
+ // Java and Go both default it to false, meaning a boolean flag resolves from
+ // Flagsmith's feature_state_value. The Python provider defaults the OPPOSITE way:
+ // it reads the `enabled` state and treats feature_state_value as opt-in.
+ //
+ // Set explicitly here, because the canonical set models booleans as values -- every
+ // flag is seeded enabled, so reading `enabled` would resolve boolean-zero-flag to
+ // true, which is exactly what the falsy-value scenario catches.
+ .usingBooleanConfigValue(true)
+ .build();
+ return new FlagsmithProvider(options);
+ }
+
+ @Override
+ public FeatureProvider createUnavailableProvider() {
+ // Pointed at a closed port on localhost, never at the backend under test -- that has to
+ // stay up, and simulated outages belong to the control API.
+ FlagsmithProviderOptions options = FlagsmithProviderOptions.builder()
+ .apiKey(SERVER_SIDE_KEY)
+ .baseUri("http://localhost:9999/api/v1/")
+ .usingBooleanConfigValue(true)
+ .build();
+ return new FlagsmithProvider(options);
+ }
+
+ /**
+ * Predictions, to be corrected by the run.
+ *
+ * {@code VARIANTS} is withheld for the reason the Go adoption established: Flagsmith has no
+ * variant concept for a plain feature, the evaluation response carries no variant key, and no
+ * seeding can produce one. That is permitted rather than defective -- 2.2.4 makes populating
+ * the variant a SHOULD -- so it carries no deviation entry.
+ *
+ *
{@code LARGE_INTEGERS} is withheld here and declared in Go, and the difference is real
+ * rather than an oversight: Java's integer accessor is a 32-bit {@code Integer}, so 2^53-1
+ * cannot be asked for at all. This is the same reason Java withholds it for flagd.
+ *
+ *
{@code STANDARD_REASONS} and {@code NUMERIC_COERCION} are declared and both fail: this
+ * provider attempts each and gets it wrong, so the failures belong in the results.
+ *
+ *
{@code STRING_TYPING} is declared and {@code FULLY_TYPED_VALUES} withheld, which is the
+ * split this backend motivated. {@code feature_state_value} is natively boolean, integer or
+ * string: a boolean really is a boolean and an integer really is an integer, so both report
+ * {@code TYPE_MISMATCH} through the String accessor, while a float and a structure have no
+ * native type, are stored as text, and are correctly returned. Withheld rather than deviated
+ * because nothing obliges a provider to report a type its store does not have.
+ *
+ *
{@code DISABLED_FLAGS} is declared. Flagsmith's native model is {@code enabled} plus a
+ * value, so the canonical set's four disabled-* flags map straight onto it.
+ *
+ *
The lifecycle and event capabilities are withheld: this provider has no observable
+ * initialisation for the suite to assert against.
+ */
+ @Override
+ public Set capabilities() {
+ return EnumSet.of(
+ Capability.OBJECT,
+ Capability.TARGETING,
+ Capability.DISABLED_FLAGS,
+ Capability.STANDARD_REASONS,
+ Capability.NUMERIC_COERCION,
+ Capability.STRING_TYPING);
+ }
+
+ @Override
+ public List knownDeviations() {
+ return Arrays.asList(
+ KnownDeviation.untracked(
+ Capability.STANDARD_REASONS,
+ "This provider reports exactly one reason. resolveFlagsmithEvaluation returns "
+ + "Reason.DISABLED for a disabled flag and leaves the reason null on every "
+ + "successful resolution, so STATIC and TARGETING_MATCH are never reported. The "
+ + "resolved values are correct throughout -- only the reason is missing. The Go "
+ + "Flagsmith provider reports all three against the identical backend, which makes "
+ + "this a gap rather than a considered choice."),
+ KnownDeviation.untracked(
+ Capability.NUMERIC_COERCION,
+ "Flagsmith stores floats as strings, and this provider does not parse them back. "
+ + "Both lossless coercion scenarios return the caller's default instead of the "
+ + "value, as does the untagged \"A float flag resolves as a float\". A Float "
+ + "accessor that parsed the stored string would fix all three; Go has one, "
+ + "Python and Java do not."));
+ }
+}
diff --git a/providers/flagsmith/src/test/resources/tck/docker-compose.yaml b/providers/flagsmith/src/test/resources/tck/docker-compose.yaml
new file mode 100644
index 0000000000..534a65ef2e
--- /dev/null
+++ b/providers/flagsmith/src/test/resources/tck/docker-compose.yaml
@@ -0,0 +1,17 @@
+# The Flagsmith provider-TCK backend: the Flagsmith Edge Proxy with a launchpad implementing the
+# control API as its upstream. https://github.com/aepfli/flagsmith-tck-testbed
+#
+# The service is named "backend" because that is ContainerizedProviderTckTest's default.
+#
+# The image is PINNED, not :latest. Four language adoptions pull this image, and a mutable tag lets
+# a push to the testbed change four pull requests' results with no diff anywhere to explain it.
+#
+# Ports are never pinned. The TCK reads the dynamically mapped host ports after the stack is up,
+# and POST /stop deliberately never restarts the container precisely so those mappings survive an
+# outage scenario.
+services:
+ backend:
+ image: ${FLAGSMITH_TESTBED_IMAGE:-ghcr.io/aepfli/flagsmith-tck-testbed:0.1.0}
+ ports:
+ - 8000 # Edge Proxy -- what the provider under test talks to
+ - 8080 # launchpad -- control API