Skip to content

Commit c37d249

Browse files
authored
feat: implement gherkin tests for context merging (#1363)
feat: implement gherkin tests for context merging (#1363)
1 parent 959e675 commit c37d249

File tree

6 files changed

+187
-7
lines changed

6 files changed

+187
-7
lines changed

src/main/java/dev/openfeature/sdk/ImmutableMetadata.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ public boolean isEmpty() {
101101
return metadata.isEmpty();
102102
}
103103

104+
public boolean isNotEmpty() {
105+
return !metadata.isEmpty();
106+
}
107+
104108
/**
105109
* Obtain a builder for {@link ImmutableMetadata}.
106110
*/

src/test/java/dev/openfeature/sdk/FlagMetadataTest.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,23 +64,25 @@ void notfound_error_validation() {
6464
}
6565

6666
@Test
67-
@DisplayName("isEmpty returns true iff the metadata is empty")
68-
void isEmpty_returns_true_if_metadata_is_empty() {
67+
@DisplayName("isEmpty and isNotEmpty return correctly when the metadata is empty")
68+
void isEmpty_isNotEmpty_return_correctly_when_metadata_is_empty() {
6969
// given
7070
ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();
7171

7272
// then
7373
assertTrue(flagMetadata.isEmpty());
74+
assertFalse(flagMetadata.isNotEmpty());
7475
}
7576

7677
@Test
77-
@DisplayName("isEmpty returns false iff the metadata is not empty")
78-
void isEmpty_returns_false_if_metadata_is_not_empty() {
78+
@DisplayName("isEmpty and isNotEmpty return correctly when the metadata is not empty")
79+
void isEmpty_isNotEmpty_return_correctly_when_metadata_is_not_empty() {
7980
// given
8081
ImmutableMetadata flagMetadata =
8182
ImmutableMetadata.builder().addString("a", "b").build();
8283

8384
// then
8485
assertFalse(flagMetadata.isEmpty());
86+
assertTrue(flagMetadata.isNotEmpty());
8587
}
8688
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package dev.openfeature.sdk.e2e;
2+
3+
import dev.openfeature.sdk.EvaluationContext;
4+
import dev.openfeature.sdk.FeatureProvider;
5+
import dev.openfeature.sdk.Metadata;
6+
import dev.openfeature.sdk.ProviderEvaluation;
7+
import dev.openfeature.sdk.Value;
8+
import lombok.Getter;
9+
10+
@Getter
11+
public class ContextStoringProvider implements FeatureProvider {
12+
private EvaluationContext evaluationContext;
13+
14+
@Override
15+
public Metadata getMetadata() {
16+
return () -> getClass().getSimpleName();
17+
}
18+
19+
@Override
20+
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
21+
this.evaluationContext = ctx;
22+
return null;
23+
}
24+
25+
@Override
26+
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
27+
this.evaluationContext = ctx;
28+
return null;
29+
}
30+
31+
@Override
32+
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
33+
this.evaluationContext = ctx;
34+
return null;
35+
}
36+
37+
@Override
38+
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
39+
this.evaluationContext = ctx;
40+
return null;
41+
}
42+
43+
@Override
44+
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
45+
this.evaluationContext = ctx;
46+
return null;
47+
}
48+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
package dev.openfeature.sdk.e2e;
22

33
import dev.openfeature.sdk.Client;
4+
import dev.openfeature.sdk.EvaluationContext;
5+
import dev.openfeature.sdk.FeatureProvider;
46
import dev.openfeature.sdk.FlagEvaluationDetails;
57
import dev.openfeature.sdk.MutableContext;
8+
import java.util.List;
69

710
public class State {
811
public Client client;
912
public Flag flag;
1013
public MutableContext context = new MutableContext();
1114
public FlagEvaluationDetails evaluation;
1215
public MockHook hook;
16+
public FeatureProvider provider;
17+
public EvaluationContext invocationContext;
18+
public List<String> levels;
1319
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package dev.openfeature.sdk.e2e.steps;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
7+
import dev.openfeature.sdk.EvaluationContext;
8+
import dev.openfeature.sdk.Hook;
9+
import dev.openfeature.sdk.HookContext;
10+
import dev.openfeature.sdk.ImmutableContext;
11+
import dev.openfeature.sdk.OpenFeatureAPI;
12+
import dev.openfeature.sdk.ThreadLocalTransactionContextPropagator;
13+
import dev.openfeature.sdk.Value;
14+
import dev.openfeature.sdk.e2e.ContextStoringProvider;
15+
import dev.openfeature.sdk.e2e.State;
16+
import io.cucumber.datatable.DataTable;
17+
import io.cucumber.java.en.And;
18+
import io.cucumber.java.en.Given;
19+
import io.cucumber.java.en.Then;
20+
import io.cucumber.java.en.When;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.Optional;
24+
25+
public class ContextSteps {
26+
private final State state;
27+
28+
public ContextSteps(State state) {
29+
this.state = state;
30+
}
31+
32+
@Given("a stable provider with retrievable context is registered")
33+
public void setup() {
34+
ContextStoringProvider provider = new ContextStoringProvider();
35+
state.provider = provider;
36+
OpenFeatureAPI.getInstance().setProviderAndWait(provider);
37+
state.client = OpenFeatureAPI.getInstance().getClient();
38+
OpenFeatureAPI.getInstance().setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator());
39+
}
40+
41+
@When("A context entry with key {string} and value {string} is added to the {string} level")
42+
public void aContextWithKeyAndValueIsAddedToTheLevel(String contextKey, String contextValue, String level) {
43+
addContextEntry(contextKey, contextValue, level);
44+
}
45+
46+
private void addContextEntry(String contextKey, String contextValue, String level) {
47+
Map<String, Value> data = new HashMap<>();
48+
data.put(contextKey, new Value(contextValue));
49+
EvaluationContext context = new ImmutableContext(data);
50+
if ("API".equals(level)) {
51+
OpenFeatureAPI.getInstance().setEvaluationContext(context);
52+
} else if ("Transaction".equals(level)) {
53+
OpenFeatureAPI.getInstance().setTransactionContext(context);
54+
} else if ("Client".equals(level)) {
55+
state.client.setEvaluationContext(context);
56+
} else if ("Invocation".equals(level)) {
57+
state.invocationContext = context;
58+
} else if ("Before Hooks".equals(level)) {
59+
state.client.addHooks(new Hook() {
60+
@Override
61+
public Optional<EvaluationContext> before(HookContext ctx, Map hints) {
62+
return Optional.of(context);
63+
}
64+
});
65+
} else {
66+
throw new IllegalArgumentException("Unknown level: " + level);
67+
}
68+
}
69+
70+
@When("Some flag was evaluated")
71+
public void someFlagWasEvaluated() {
72+
state.evaluation = state.client.getStringDetails("unused", "unused", state.invocationContext);
73+
}
74+
75+
@Then("The merged context contains an entry with key {string} and value {string}")
76+
public void theMergedContextContainsAnEntryWithKeyAndValue(String contextKey, String contextValue) {
77+
assertInstanceOf(
78+
ContextStoringProvider.class,
79+
state.provider,
80+
"In order to use this step, you need to set a ContextStoringProvider");
81+
EvaluationContext ctx = ((ContextStoringProvider) state.provider).getEvaluationContext();
82+
assertNotNull(ctx);
83+
assertNotNull(ctx.getValue(contextKey));
84+
assertNotNull(ctx.getValue(contextKey).asString());
85+
assertEquals(contextValue, ctx.getValue(contextKey).asString());
86+
}
87+
88+
@Given("A table with levels of increasing precedence")
89+
public void aTableWithLevelsOfIncreasingPrecedence(DataTable levelsTable) {
90+
state.levels = levelsTable.asList();
91+
}
92+
93+
@And(
94+
"Context entries for each level from API level down to the {string} level, with key {string} and value {string}")
95+
public void contextEntriesForEachLevelFromAPILevelDownToTheLevelWithKeyAndValue(
96+
String maxLevel, String key, String value) {
97+
for (String level : state.levels) {
98+
addContextEntry(key, value, level);
99+
if (level.equals(maxLevel)) {
100+
return;
101+
}
102+
}
103+
}
104+
}

src/test/java/dev/openfeature/sdk/e2e/steps/StepDefinitions.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags;
44
import static org.junit.jupiter.api.Assertions.assertEquals;
5-
import static org.junit.jupiter.api.Assertions.assertTrue;
65

76
import dev.openfeature.sdk.Client;
87
import dev.openfeature.sdk.EvaluationContext;
@@ -289,7 +288,7 @@ public void then_the_default_string_value_should_be_returned() {
289288
@Then("the reason should indicate an error and the error code should indicate a missing flag with {string}")
290289
public void the_reason_should_indicate_an_error_and_the_error_code_should_be_flag_not_found(String errorCode) {
291290
assertEquals(Reason.ERROR.toString(), notFoundDetails.getReason());
292-
assertTrue(notFoundDetails.getErrorCode().name().equals(errorCode));
291+
assertEquals(errorCode, notFoundDetails.getErrorCode().name());
293292
}
294293

295294
// type mismatch
@@ -309,6 +308,23 @@ public void then_the_default_integer_value_should_be_returned() {
309308
@Then("the reason should indicate an error and the error code should indicate a type mismatch with {string}")
310309
public void the_reason_should_indicate_an_error_and_the_error_code_should_be_type_mismatch(String errorCode) {
311310
assertEquals(Reason.ERROR.toString(), typeErrorDetails.getReason());
312-
assertTrue(typeErrorDetails.getErrorCode().name().equals(errorCode));
311+
assertEquals(errorCode, typeErrorDetails.getErrorCode().name());
312+
}
313+
314+
@SuppressWarnings("java:S2925")
315+
@When("sleep for {int} milliseconds")
316+
public void sleepForMilliseconds(int millis) {
317+
long startTime = System.currentTimeMillis();
318+
long endTime = startTime + millis;
319+
long now;
320+
while ((now = System.currentTimeMillis()) < endTime) {
321+
long remainingTime = endTime - now;
322+
try {
323+
//noinspection BusyWait
324+
Thread.sleep(remainingTime);
325+
} catch (InterruptedException e) {
326+
throw new RuntimeException(e);
327+
}
328+
}
313329
}
314330
}

0 commit comments

Comments
 (0)