Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}/cache:main
secrets: |
"js_e2e_test_env=${{ secrets.JS_E2E_TEST_ENV }}"
"java_e2e_test_env=${{ secrets.JS_E2E_TEST_ENV }}"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reusing the js e2e test env file due to laziness.

- name: Build and test everything (Push - updates cache)
if: github.event_name == 'push'
Expand All @@ -51,3 +52,4 @@ jobs:
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}/cache:main,mode=max
secrets: |
"js_e2e_test_env=${{ secrets.JS_E2E_TEST_ENV }}"
"java_e2e_test_env=${{ secrets.JS_E2E_TEST_ENV }}"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ wasm/confidence_resolver.wasm
# But DO track WASM embedded in Go provider (committed for go:embed)
!openfeature-provider/go/wasm/confidence_resolver.wasm

# Ignore e2e test environment files (contain credentials)
.env.test


10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,15 @@ FROM openfeature-provider-java-base AS openfeature-provider-java.test

RUN make test

# ==============================================================================
# E2E Test OpenFeature Provider (Java) (requires credentials)
# ==============================================================================
FROM openfeature-provider-java.test AS openfeature-provider-java.test_e2e

# Run e2e tests with secrets mounted as .env.test file
RUN --mount=type=secret,id=java_e2e_test_env,target=.env.test \
make test-e2e

# ==============================================================================
# Build OpenFeature Provider (Java)
# ==============================================================================
Expand Down Expand Up @@ -600,6 +609,7 @@ COPY --from=wasm-msg.test /workspace/Cargo.toml /markers/test-wasm-msg
COPY --from=openfeature-provider-js.test /app/package.json /markers/test-openfeature-js
COPY --from=openfeature-provider-js.test_e2e /app/package.json /markers/test-openfeature-js-e2e
COPY --from=openfeature-provider-java.test /app/pom.xml /markers/test-openfeature-java
COPY --from=openfeature-provider-java.test_e2e /app/pom.xml /markers/test-openfeature-java-e2e
COPY --from=openfeature-provider-go.test /app/go.mod /markers/test-openfeature-go

# Force validation stages to run
Expand Down
14 changes: 12 additions & 2 deletions openfeature-provider/java/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ SRC := $(shell find src -name '*.java' 2>/dev/null)
RESOURCES_WASM := src/main/resources/wasm/confidence_resolver.wasm
LOCAL_WASM := $(ROOT)/wasm/confidence_resolver.wasm

.PHONY: build test clean
.PHONY: build test test-e2e clean

# Always build wasm if not in docker
ifneq ($(IN_DOCKER_BUILD),1)
Expand All @@ -30,7 +30,17 @@ $(BUILD_STAMP): pom.xml $(RESOURCES_WASM) $(SRC)
build: $(BUILD_STAMP)

test: $(BUILD_STAMP)
mvn -q test
mvn -q test -Dtest='!**/*E2ETest'

test-e2e: $(BUILD_STAMP)
@if [ ! -f .env.test ]; then \
echo "Warning: .env.test file not found. E2E tests may fail."; \
fi
@if [ -f .env.test ]; then \
export $$(cat .env.test | xargs) && mvn -q test -Dtest='**/*E2ETest'; \
else \
mvn -q test -Dtest='**/*E2ETest'; \
fi

clean:
mvn -q clean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class FlagsAdminStateFetcher {
private final AtomicReference<ResolverStateUriResponse> resolverStateUriResponse =
new AtomicReference<>();
private final AtomicReference<Instant> refreshTimeHolder = new AtomicReference<>();
String accountId;
String accountId = "";

public FlagsAdminStateFetcher(
ResolverStateServiceGrpc.ResolverStateServiceBlockingStub resolverStateService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ private static FlagResolverService createFlagResolverService(
final HealthStatus healthStatus = new HealthStatus(healthStatusManager);
final FlagsAdminStateFetcher sidecarFlagsAdminFetcher =
new FlagsAdminStateFetcher(resolverStateService, healthStatus, token.account());
// Perform initial reload to fetch state and set accountId before creating resolver
sidecarFlagsAdminFetcher.reload();
final long pollIntervalSeconds = getPollIntervalSeconds();
final var wasmFlagLogger = new GrpcWasmFlagLogger(apiSecret);
final ResolverApi wasmResolverApi =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.spotify.confidence;

import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;

import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FlagEvaluationDetails;
import dev.openfeature.sdk.MutableContext;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.Value;
import java.util.Map;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

/**
* End-to-end tests for OpenFeatureLocalResolveProvider.
*
* <p>These tests verify the provider against real Confidence service flags. They require valid API
* credentials to be set as environment variables.
*
* <p>Required environment variables:
*
* <ul>
* <li>JAVA_E2E_CONFIDENCE_API_CLIENT_ID - API client ID for authentication
* <li>JAVA_E2E_CONFIDENCE_API_CLIENT_SECRET - API client secret for authentication
* </ul>
*/
class OpenFeatureLocalResolveProviderE2ETest {
private static final String FLAG_CLIENT_SECRET = "RxDVTrXvc6op1XxiQ4OaR31dKbJ39aYV";
private static OpenFeatureLocalResolveProvider provider;
private static Client client;

@BeforeAll
static void setup() {
final String apiClientId = requireEnv("JS_E2E_CONFIDENCE_API_CLIENT_ID");
final String apiClientSecret = requireEnv("JS_E2E_CONFIDENCE_API_CLIENT_SECRET");

final ApiSecret apiSecret = new ApiSecret(apiClientId, apiClientSecret);

provider = new OpenFeatureLocalResolveProvider(apiSecret, FLAG_CLIENT_SECRET);

final OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProviderAndWait(provider);

// Set evaluation context with targeting key
final EvaluationContext context = new MutableContext("test-a");
api.setEvaluationContext(context);

client = api.getClient();
}

@AfterAll
static void teardown() {
if (provider != null) {
provider.shutdown();
}
OpenFeatureAPI.getInstance().shutdown();
}

@Test
void shouldResolveBoolean() {
final boolean value = client.getBooleanValue("web-sdk-e2e-flag.bool", true);
assertThat(value).isFalse();
}

@Test
void shouldResolveInt() {
final int value = client.getIntegerValue("web-sdk-e2e-flag.int", 10);
assertEquals(3, value);
}

@Test
void shouldResolveDouble() {
final double value = client.getDoubleValue("web-sdk-e2e-flag.double", 10.0);
assertEquals(3.5, value, 0.001);
}

@Test
void shouldResolveString() {
final String value = client.getStringValue("web-sdk-e2e-flag.str", "default");
assertEquals("control", value);
}

@Test
void shouldResolveStruct() {
final Value value = client.getObjectValue("web-sdk-e2e-flag.obj", new Value());

assertThat(value.isStructure()).isTrue();
final Map<String, Value> struct = value.asStructure().asMap();

assertEquals(4, struct.get("int").asInteger());
assertEquals("obj control", struct.get("str").asString());
assertThat(struct.get("bool").asBoolean()).isFalse();
assertEquals(3.6, struct.get("double").asDouble(), 0.001);
assertThat(struct.get("obj-obj").asStructure().asMap()).isEmpty();
}

@Test
void shouldResolveSubValueFromStruct() {
final boolean value = client.getBooleanValue("web-sdk-e2e-flag.obj.bool", true);
assertThat(value).isFalse();
}

@Test
void shouldResolveSubValueFromStructWithDetails() {
final FlagEvaluationDetails<Double> details =
client.getDoubleDetails("web-sdk-e2e-flag.obj.double", 1.0);

assertEquals(3.6, details.getValue(), 0.001);
assertEquals("flags/web-sdk-e2e-flag/variants/control", details.getVariant());
assertEquals("RESOLVE_REASON_MATCH", details.getReason());
}

@Test
void shouldResolveFlagWithStickyResolve() {
final EvaluationContext stickyContext =
new MutableContext("test-a")
.add("sticky", true);

final FlagEvaluationDetails<Double> details =
client.getDoubleDetails("web-sdk-e2e-flag.double", -1.0, stickyContext);

// The flag has a running experiment with a sticky assignment. The intake is paused but we
// should still get the sticky assignment.
// If this test breaks it could mean that the experiment was removed or that the bigtable
// materialization was cleaned out.
assertEquals(99.99, details.getValue(), 0.001);
assertEquals("flags/web-sdk-e2e-flag/variants/sticky", details.getVariant());
assertEquals("RESOLVE_REASON_MATCH", details.getReason());
}

private static String requireEnv(String name) {
final String value = System.getenv(name);
if (value == null || value.isEmpty()) {
throw new IllegalStateException(
String.format("Missing required environment variable: %s", name));
}
return value;
}
}
Loading