Skip to content

Commit 93581bd

Browse files
authored
fix(java): reload state before creating the initial Resolver (#104)
1 parent 79f48cd commit 93581bd

File tree

7 files changed

+172
-3
lines changed

7 files changed

+172
-3
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ jobs:
3939
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}/cache:main
4040
secrets: |
4141
"js_e2e_test_env=${{ secrets.JS_E2E_TEST_ENV }}"
42+
"java_e2e_test_env=${{ secrets.JS_E2E_TEST_ENV }}"
4243
4344
- name: Build and test everything (Push - updates cache)
4445
if: github.event_name == 'push'
@@ -51,3 +52,4 @@ jobs:
5152
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}/cache:main,mode=max
5253
secrets: |
5354
"js_e2e_test_env=${{ secrets.JS_E2E_TEST_ENV }}"
55+
"java_e2e_test_env=${{ secrets.JS_E2E_TEST_ENV }}"

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ wasm/confidence_resolver.wasm
1414
# But DO track WASM embedded in Go provider (committed for go:embed)
1515
!openfeature-provider/go/wasm/confidence_resolver.wasm
1616

17+
# Ignore e2e test environment files (contain credentials)
18+
.env.test
19+
1720

Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,15 @@ FROM openfeature-provider-java-base AS openfeature-provider-java.test
567567

568568
RUN make test
569569

570+
# ==============================================================================
571+
# E2E Test OpenFeature Provider (Java) (requires credentials)
572+
# ==============================================================================
573+
FROM openfeature-provider-java.test AS openfeature-provider-java.test_e2e
574+
575+
# Run e2e tests with secrets mounted as .env.test file
576+
RUN --mount=type=secret,id=java_e2e_test_env,target=.env.test \
577+
make test-e2e
578+
570579
# ==============================================================================
571580
# Build OpenFeature Provider (Java)
572581
# ==============================================================================
@@ -600,6 +609,7 @@ COPY --from=wasm-msg.test /workspace/Cargo.toml /markers/test-wasm-msg
600609
COPY --from=openfeature-provider-js.test /app/package.json /markers/test-openfeature-js
601610
COPY --from=openfeature-provider-js.test_e2e /app/package.json /markers/test-openfeature-js-e2e
602611
COPY --from=openfeature-provider-java.test /app/pom.xml /markers/test-openfeature-java
612+
COPY --from=openfeature-provider-java.test_e2e /app/pom.xml /markers/test-openfeature-java-e2e
603613
COPY --from=openfeature-provider-go.test /app/go.mod /markers/test-openfeature-go
604614

605615
# Force validation stages to run

openfeature-provider/java/Makefile

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ SRC := $(shell find src -name '*.java' 2>/dev/null)
99
RESOURCES_WASM := src/main/resources/wasm/confidence_resolver.wasm
1010
LOCAL_WASM := $(ROOT)/wasm/confidence_resolver.wasm
1111

12-
.PHONY: build test clean
12+
.PHONY: build test test-e2e clean
1313

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

3232
test: $(BUILD_STAMP)
33-
mvn -q test
33+
mvn -q test -Dtest='!**/*E2ETest'
34+
35+
test-e2e: $(BUILD_STAMP)
36+
@if [ ! -f .env.test ]; then \
37+
echo "Warning: .env.test file not found. E2E tests may fail."; \
38+
fi
39+
@if [ -f .env.test ]; then \
40+
export $$(cat .env.test | xargs) && mvn -q test -Dtest='**/*E2ETest'; \
41+
else \
42+
mvn -q test -Dtest='**/*E2ETest'; \
43+
fi
3444

3545
clean:
3646
mvn -q clean

openfeature-provider/java/src/main/java/com/spotify/confidence/FlagsAdminStateFetcher.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class FlagsAdminStateFetcher {
3232
private final AtomicReference<ResolverStateUriResponse> resolverStateUriResponse =
3333
new AtomicReference<>();
3434
private final AtomicReference<Instant> refreshTimeHolder = new AtomicReference<>();
35-
String accountId;
35+
String accountId = "";
3636

3737
public FlagsAdminStateFetcher(
3838
ResolverStateServiceGrpc.ResolverStateServiceBlockingStub resolverStateService,

openfeature-provider/java/src/main/java/com/spotify/confidence/LocalResolverServiceFactory.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ private static FlagResolverService createFlagResolverService(
6363
final HealthStatus healthStatus = new HealthStatus(healthStatusManager);
6464
final FlagsAdminStateFetcher sidecarFlagsAdminFetcher =
6565
new FlagsAdminStateFetcher(resolverStateService, healthStatus, token.account());
66+
// Perform initial reload to fetch state and set accountId before creating resolver
67+
sidecarFlagsAdminFetcher.reload();
6668
final long pollIntervalSeconds = getPollIntervalSeconds();
6769
final var wasmFlagLogger = new GrpcWasmFlagLogger(apiSecret);
6870
final ResolverApi wasmResolverApi =
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package com.spotify.confidence;
2+
3+
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
6+
import dev.openfeature.sdk.Client;
7+
import dev.openfeature.sdk.EvaluationContext;
8+
import dev.openfeature.sdk.FlagEvaluationDetails;
9+
import dev.openfeature.sdk.MutableContext;
10+
import dev.openfeature.sdk.OpenFeatureAPI;
11+
import dev.openfeature.sdk.Value;
12+
import java.util.Map;
13+
import org.junit.jupiter.api.AfterAll;
14+
import org.junit.jupiter.api.BeforeAll;
15+
import org.junit.jupiter.api.Test;
16+
17+
/**
18+
* End-to-end tests for OpenFeatureLocalResolveProvider.
19+
*
20+
* <p>These tests verify the provider against real Confidence service flags. They require valid API
21+
* credentials to be set as environment variables.
22+
*
23+
* <p>Required environment variables:
24+
*
25+
* <ul>
26+
* <li>JAVA_E2E_CONFIDENCE_API_CLIENT_ID - API client ID for authentication
27+
* <li>JAVA_E2E_CONFIDENCE_API_CLIENT_SECRET - API client secret for authentication
28+
* </ul>
29+
*/
30+
class OpenFeatureLocalResolveProviderE2ETest {
31+
private static final String FLAG_CLIENT_SECRET = "RxDVTrXvc6op1XxiQ4OaR31dKbJ39aYV";
32+
private static OpenFeatureLocalResolveProvider provider;
33+
private static Client client;
34+
35+
@BeforeAll
36+
static void setup() {
37+
final String apiClientId = requireEnv("JS_E2E_CONFIDENCE_API_CLIENT_ID");
38+
final String apiClientSecret = requireEnv("JS_E2E_CONFIDENCE_API_CLIENT_SECRET");
39+
40+
final ApiSecret apiSecret = new ApiSecret(apiClientId, apiClientSecret);
41+
42+
provider = new OpenFeatureLocalResolveProvider(apiSecret, FLAG_CLIENT_SECRET);
43+
44+
final OpenFeatureAPI api = OpenFeatureAPI.getInstance();
45+
api.setProviderAndWait(provider);
46+
47+
// Set evaluation context with targeting key
48+
final EvaluationContext context = new MutableContext("test-a");
49+
api.setEvaluationContext(context);
50+
51+
client = api.getClient();
52+
}
53+
54+
@AfterAll
55+
static void teardown() {
56+
if (provider != null) {
57+
provider.shutdown();
58+
}
59+
OpenFeatureAPI.getInstance().shutdown();
60+
}
61+
62+
@Test
63+
void shouldResolveBoolean() {
64+
final boolean value = client.getBooleanValue("web-sdk-e2e-flag.bool", true);
65+
assertThat(value).isFalse();
66+
}
67+
68+
@Test
69+
void shouldResolveInt() {
70+
final int value = client.getIntegerValue("web-sdk-e2e-flag.int", 10);
71+
assertEquals(3, value);
72+
}
73+
74+
@Test
75+
void shouldResolveDouble() {
76+
final double value = client.getDoubleValue("web-sdk-e2e-flag.double", 10.0);
77+
assertEquals(3.5, value, 0.001);
78+
}
79+
80+
@Test
81+
void shouldResolveString() {
82+
final String value = client.getStringValue("web-sdk-e2e-flag.str", "default");
83+
assertEquals("control", value);
84+
}
85+
86+
@Test
87+
void shouldResolveStruct() {
88+
final Value value = client.getObjectValue("web-sdk-e2e-flag.obj", new Value());
89+
90+
assertThat(value.isStructure()).isTrue();
91+
final Map<String, Value> struct = value.asStructure().asMap();
92+
93+
assertEquals(4, struct.get("int").asInteger());
94+
assertEquals("obj control", struct.get("str").asString());
95+
assertThat(struct.get("bool").asBoolean()).isFalse();
96+
assertEquals(3.6, struct.get("double").asDouble(), 0.001);
97+
assertThat(struct.get("obj-obj").asStructure().asMap()).isEmpty();
98+
}
99+
100+
@Test
101+
void shouldResolveSubValueFromStruct() {
102+
final boolean value = client.getBooleanValue("web-sdk-e2e-flag.obj.bool", true);
103+
assertThat(value).isFalse();
104+
}
105+
106+
@Test
107+
void shouldResolveSubValueFromStructWithDetails() {
108+
final FlagEvaluationDetails<Double> details =
109+
client.getDoubleDetails("web-sdk-e2e-flag.obj.double", 1.0);
110+
111+
assertEquals(3.6, details.getValue(), 0.001);
112+
assertEquals("flags/web-sdk-e2e-flag/variants/control", details.getVariant());
113+
assertEquals("RESOLVE_REASON_MATCH", details.getReason());
114+
}
115+
116+
@Test
117+
void shouldResolveFlagWithStickyResolve() {
118+
final EvaluationContext stickyContext =
119+
new MutableContext("test-a")
120+
.add("sticky", true);
121+
122+
final FlagEvaluationDetails<Double> details =
123+
client.getDoubleDetails("web-sdk-e2e-flag.double", -1.0, stickyContext);
124+
125+
// The flag has a running experiment with a sticky assignment. The intake is paused but we
126+
// should still get the sticky assignment.
127+
// If this test breaks it could mean that the experiment was removed or that the bigtable
128+
// materialization was cleaned out.
129+
assertEquals(99.99, details.getValue(), 0.001);
130+
assertEquals("flags/web-sdk-e2e-flag/variants/sticky", details.getVariant());
131+
assertEquals("RESOLVE_REASON_MATCH", details.getReason());
132+
}
133+
134+
private static String requireEnv(String name) {
135+
final String value = System.getenv(name);
136+
if (value == null || value.isEmpty()) {
137+
throw new IllegalStateException(
138+
String.format("Missing required environment variable: %s", name));
139+
}
140+
return value;
141+
}
142+
}

0 commit comments

Comments
 (0)