diff --git a/build.gradle b/build.gradle
index 35613f76..ded52ad4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -62,11 +62,17 @@ java {
withSourcesJar()
}
+tasks.register('testJar', Jar) {
+ archiveClassifier.set('tests')
+ from sourceSets.test.output
+}
+
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = 'sdk-common-jvm'
from components.java
+ artifact testJar // Include the test-jar in the published artifacts
versionMapping {
usage('java-api') {
fromResolutionOf('runtimeClasspath')
@@ -114,7 +120,7 @@ publishing {
// Custom task to ensure we can conditionally publish either a release or snapshot artifact
// based on a command line switch. See github workflow files for more details on usage.
-task checkVersion {
+tasks.register('checkVersion') {
doLast {
if (!project.hasProperty('release') && !project.hasProperty('snapshot')) {
throw new GradleException("You must specify either -Prelease or -Psnapshot")
@@ -135,7 +141,7 @@ tasks.named('publish').configure {
}
// Conditionally enable or disable publishing tasks
-tasks.withType(PublishToMavenRepository) {
+tasks.withType(PublishToMavenRepository).configureEach {
onlyIf {
project.ext.has('shouldPublish') && project.ext.shouldPublish
}
diff --git a/src/main/java/cloud/eppo/BaseEppoClient.java b/src/main/java/cloud/eppo/BaseEppoClient.java
index 295ca91e..8ed889ec 100644
--- a/src/main/java/cloud/eppo/BaseEppoClient.java
+++ b/src/main/java/cloud/eppo/BaseEppoClient.java
@@ -22,11 +22,11 @@ public class BaseEppoClient {
private final ObjectMapper mapper =
new ObjectMapper()
.registerModule(EppoModule.eppoModule()); // TODO: is this the best place for this?
- private static final String DEFAULT_HOST = "https://fscdn.eppo.cloud";
- private static final boolean DEFAULT_IS_GRACEFUL_MODE = true;
+
+ protected static final String DEFAULT_HOST = "https://fscdn.eppo.cloud";
+ protected final ConfigurationRequestor requestor;
private final ConfigurationStore configurationStore;
- private final ConfigurationRequestor requestor;
private final AssignmentLogger assignmentLogger;
private final BanditLogger banditLogger;
private final String sdkName;
@@ -34,25 +34,34 @@ public class BaseEppoClient {
private final boolean isConfigObfuscated;
private boolean isGracefulMode;
- private static BaseEppoClient instance;
-
// Fields useful for testing in situations where we want to mock the http client or configuration
// store (accessed via reflection)
/** @noinspection FieldMayBeFinal */
private static EppoHttpClient httpClientOverride = null;
- private BaseEppoClient(
+ protected BaseEppoClient(
String apiKey,
String sdkName,
String sdkVersion,
String host,
- ConfigurationStore configurationStore,
AssignmentLogger assignmentLogger,
BanditLogger banditLogger,
- boolean isGracefulMode) {
+ boolean isGracefulMode,
+ boolean expectObfuscatedConfig) {
+
+ if (apiKey == null) {
+ throw new IllegalArgumentException("Unable to initialize Eppo SDK due to missing API key");
+ }
+ if (sdkName == null || sdkVersion == null) {
+ throw new IllegalArgumentException(
+ "Unable to initialize Eppo SDK due to missing SDK name or version");
+ }
+ if (host == null) {
+ host = DEFAULT_HOST;
+ }
EppoHttpClient httpClient = buildHttpClient(host, apiKey, sdkName, sdkVersion);
- this.configurationStore = configurationStore;
+ this.configurationStore = new ConfigurationStore();
requestor = new ConfigurationRequestor(configurationStore, httpClient);
this.assignmentLogger = assignmentLogger;
this.banditLogger = banditLogger;
@@ -61,8 +70,10 @@ private BaseEppoClient(
this.sdkName = sdkName;
this.sdkVersion = sdkVersion;
// For now, the configuration is only obfuscated for Android clients
- this.isConfigObfuscated = sdkName.toLowerCase().contains("android");
+ this.isConfigObfuscated = expectObfuscatedConfig;
+
// TODO: caching initialization (such as setting an API-key-specific prefix
+ // will probably involve passing in configurationStore to the constructor
}
private EppoHttpClient buildHttpClient(
@@ -78,51 +89,7 @@ private EppoHttpClient buildHttpClient(
return httpClient;
}
- public static BaseEppoClient init(
- String apiKey,
- String sdkName,
- String sdkVersion,
- String host,
- AssignmentLogger assignmentLogger,
- BanditLogger banditLogger,
- boolean isGracefulMode) {
-
- if (apiKey == null) {
- throw new IllegalArgumentException("Unable to initialize Eppo SDK due to missing API key");
- }
- if (sdkName == null || sdkVersion == null) {
- throw new IllegalArgumentException(
- "Unable to initialize Eppo SDK due to missing SDK name or version");
- }
-
- if (instance != null) {
- // TODO: also check we're not running a test
- log.warn("Reinitializing an Eppo Client instance that was already initialized");
- }
- instance =
- new BaseEppoClient(
- apiKey,
- sdkName,
- sdkVersion,
- host,
- new ConfigurationStore(),
- assignmentLogger,
- banditLogger,
- isGracefulMode);
- instance.refreshConfiguration();
-
- return instance;
- }
-
- /**
- * Ability to ad-hoc kick off a configuration load. Will load from a filesystem cached file as
- * well as fire off an HTTPS request for an updated configuration. If the cache load finishes
- * first, those assignments will be used until the fetch completes.
- *
- *
Deprecated, as we plan to make a more targeted and configurable way to do so in the future.
- */
- @Deprecated
- public void refreshConfiguration() {
+ protected void loadConfiguration() {
requestor.load();
}
@@ -479,65 +446,7 @@ private T throwIfNotGraceful(Exception e, T defaultValue) {
throw new RuntimeException(e);
}
- public static BaseEppoClient getInstance() {
- if (BaseEppoClient.instance == null) {
- throw new IllegalStateException("Eppo SDK has not been initialized");
- }
-
- return BaseEppoClient.instance;
- }
-
public void setIsGracefulFailureMode(boolean isGracefulFailureMode) {
this.isGracefulMode = isGracefulFailureMode;
}
-
- public static class Builder {
- private String apiKey;
- private String sdkName;
- private String sdkVersion;
- private String host = DEFAULT_HOST;
- private AssignmentLogger assignmentLogger;
- private BanditLogger banditLogger;
- private boolean isGracefulMode = DEFAULT_IS_GRACEFUL_MODE;
-
- public Builder apiKey(String apiKey) {
- this.apiKey = apiKey;
- return this;
- }
-
- public Builder sdkName(String sdkName) {
- this.sdkName = sdkName;
- return this;
- }
-
- public Builder sdkVersion(String sdkVersion) {
- this.sdkVersion = sdkVersion;
- return this;
- }
-
- public Builder host(String host) {
- this.host = host;
- return this;
- }
-
- public Builder assignmentLogger(AssignmentLogger assignmentLogger) {
- this.assignmentLogger = assignmentLogger;
- return this;
- }
-
- public Builder banditLogger(BanditLogger banditLogger) {
- this.banditLogger = banditLogger;
- return this;
- }
-
- public Builder isGracefulMode(boolean isGracefulMode) {
- this.isGracefulMode = isGracefulMode;
- return this;
- }
-
- public BaseEppoClient buildAndInit() {
- return BaseEppoClient.init(
- apiKey, sdkName, sdkVersion, host, assignmentLogger, banditLogger, isGracefulMode);
- }
- }
}
diff --git a/src/main/java/cloud/eppo/EppoHttpClient.java b/src/main/java/cloud/eppo/EppoHttpClient.java
index 6daf2fe7..54d66db4 100644
--- a/src/main/java/cloud/eppo/EppoHttpClient.java
+++ b/src/main/java/cloud/eppo/EppoHttpClient.java
@@ -59,7 +59,7 @@ public Response get(String path) {
}
}
- public void get(String path, RequestCallback callback) {
+ public void get(String path, EppoHttpClientRequestCallback callback) {
HttpUrl httpUrl =
HttpUrl.parse(baseUrl + path)
.newBuilder()
@@ -83,13 +83,11 @@ public void onResponse(Call call, Response response) {
callback.onFailure("Failed to read response from URL " + httpUrl);
}
} else {
- switch (response.code()) {
- case HttpURLConnection.HTTP_FORBIDDEN:
- callback.onFailure("Invalid API key");
- break;
- default:
- log.debug("Fetch failed with status code: " + response.code());
- callback.onFailure("Bad response from URL " + httpUrl);
+ if (response.code() == HttpURLConnection.HTTP_FORBIDDEN) {
+ callback.onFailure("Invalid API key");
+ } else {
+ log.debug("Fetch failed with status code: {}", response.code());
+ callback.onFailure("Bad response from URL " + httpUrl);
}
}
response.close();
@@ -98,19 +96,12 @@ public void onResponse(Call call, Response response) {
@Override
public void onFailure(Call call, IOException e) {
log.error(
- "Http request failure: "
- + e.getMessage()
- + " "
- + Arrays.toString(e.getStackTrace()),
+ "Http request failure: {} {}",
+ e.getMessage(),
+ Arrays.toString(e.getStackTrace()),
e);
callback.onFailure("Unable to fetch from URL " + httpUrl);
}
});
}
}
-
-interface RequestCallback {
- void onSuccess(String responseBody);
-
- void onFailure(String errorMessage);
-}
diff --git a/src/main/java/cloud/eppo/EppoHttpClientRequestCallback.java b/src/main/java/cloud/eppo/EppoHttpClientRequestCallback.java
new file mode 100644
index 00000000..6d67a2c4
--- /dev/null
+++ b/src/main/java/cloud/eppo/EppoHttpClientRequestCallback.java
@@ -0,0 +1,7 @@
+package cloud.eppo;
+
+public interface EppoHttpClientRequestCallback {
+ void onSuccess(String responseBody);
+
+ void onFailure(String errorMessage);
+}
diff --git a/src/main/java/cloud/eppo/ufc/dto/DiscriminableAttributes.java b/src/main/java/cloud/eppo/ufc/dto/DiscriminableAttributes.java
index 48d9432c..80b5ee3e 100644
--- a/src/main/java/cloud/eppo/ufc/dto/DiscriminableAttributes.java
+++ b/src/main/java/cloud/eppo/ufc/dto/DiscriminableAttributes.java
@@ -2,9 +2,9 @@
public interface DiscriminableAttributes {
- public Attributes getNumericAttributes();
+ Attributes getNumericAttributes();
- public Attributes getCategoricalAttributes();
+ Attributes getCategoricalAttributes();
- public Attributes getAllAttributes();
+ Attributes getAllAttributes();
}
diff --git a/src/test/java/cloud/eppo/BaseEppoClientBanditTest.java b/src/test/java/cloud/eppo/BaseEppoClientBanditTest.java
index b1463419..5d93c1af 100644
--- a/src/test/java/cloud/eppo/BaseEppoClientBanditTest.java
+++ b/src/test/java/cloud/eppo/BaseEppoClientBanditTest.java
@@ -1,5 +1,7 @@
package cloud.eppo;
+import static cloud.eppo.helpers.BanditTestCase.parseBanditTestCaseFile;
+import static cloud.eppo.helpers.BanditTestCase.runBanditTestCase;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@@ -9,13 +11,9 @@
import cloud.eppo.logging.BanditAssignment;
import cloud.eppo.logging.BanditLogger;
import cloud.eppo.ufc.dto.*;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.File;
-import java.io.IOException;
import java.util.*;
import java.util.stream.Stream;
-import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -33,26 +31,29 @@ public class BaseEppoClientBanditTest {
"dummy-bandits-api-key"; // Will load bandit-flags-v1
private static final String TEST_HOST =
"https://us-central1-eppo-qa.cloudfunctions.net/serveGitHubRacTestFile";
- private static final ObjectMapper mapper = new ObjectMapper().registerModule(module());
private static final AssignmentLogger mockAssignmentLogger = mock(AssignmentLogger.class);
private static final BanditLogger mockBanditLogger = mock(BanditLogger.class);
private static final Date testStart = new Date();
+ private static BaseEppoClient eppoClient;
+
// TODO: possibly consolidate code between this and the non-bandit test
@BeforeAll
public static void initClient() {
-
- new BaseEppoClient.Builder()
- .apiKey(DUMMY_BANDIT_API_KEY)
- .sdkName("java")
- .sdkVersion("3.0.0")
- .isGracefulMode(false)
- .host(TEST_HOST)
- .assignmentLogger(mockAssignmentLogger)
- .banditLogger(mockBanditLogger)
- .buildAndInit();
+ eppoClient =
+ new BaseEppoClient(
+ DUMMY_BANDIT_API_KEY,
+ "java",
+ "3.0.0",
+ TEST_HOST,
+ mockAssignmentLogger,
+ mockBanditLogger,
+ false,
+ false);
+
+ eppoClient.loadConfiguration();
log.info("Test client initialized");
}
@@ -62,82 +63,21 @@ public void reset() {
clearInvocations(mockAssignmentLogger);
clearInvocations(mockBanditLogger);
doNothing().when(mockBanditLogger).logBanditAssignment(any());
- BaseEppoClient.getInstance().setIsGracefulFailureMode(false);
+ eppoClient.setIsGracefulFailureMode(false);
}
@ParameterizedTest
@MethodSource("getBanditTestData")
public void testUnobfuscatedBanditAssignments(File testFile) {
- BanditTestCase testCase = parseTestCaseFile(testFile);
- runBanditTestCase(testCase);
- }
-
- private static Stream getBanditTestData() {
- File testCaseFolder = new File("src/test/resources/shared/ufc/bandit-tests");
- File[] testCaseFiles = testCaseFolder.listFiles();
- assertNotNull(testCaseFiles);
- assertTrue(testCaseFiles.length > 0);
- List arguments = new ArrayList<>();
- for (File testCaseFile : testCaseFiles) {
- arguments.add(Arguments.of(testCaseFile));
- }
- return arguments.stream();
- }
-
- private BanditTestCase parseTestCaseFile(File testCaseFile) {
- BanditTestCase testCase;
- try {
- String json = FileUtils.readFileToString(testCaseFile, "UTF8");
- testCase = mapper.readValue(json, BanditTestCase.class);
- } catch (IOException ex) {
- throw new RuntimeException(ex);
- }
- return testCase;
- }
-
- private void runBanditTestCase(BanditTestCase testCase) {
- assertFalse(testCase.getSubjects().isEmpty());
-
- String flagKey = testCase.getFlag();
- String defaultValue = testCase.getDefaultValue();
-
- for (BanditSubjectAssignment subjectAssignment : testCase.getSubjects()) {
- String subjectKey = subjectAssignment.getSubjectKey();
- ContextAttributes attributes = subjectAssignment.getSubjectAttributes();
- Actions actions = subjectAssignment.getActions();
- BanditResult assignment =
- BaseEppoClient.getInstance()
- .getBanditAction(flagKey, subjectKey, attributes, actions, defaultValue);
- assertBanditAssignment(flagKey, subjectAssignment, assignment);
- }
+ BanditTestCase testCase = parseBanditTestCaseFile(testFile);
+ runBanditTestCase(testCase, eppoClient);
}
- /** Helper method for asserting a bandit assignment with a useful failure message. */
- private void assertBanditAssignment(
- String flagKey, BanditSubjectAssignment expectedSubjectAssignment, BanditResult assignment) {
- String failureMessage =
- "Incorrect "
- + flagKey
- + " variation assignment for subject "
- + expectedSubjectAssignment.getSubjectKey();
-
- assertEquals(
- expectedSubjectAssignment.getAssignment().getVariation(),
- assignment.getVariation(),
- failureMessage);
-
- failureMessage =
- "Incorrect "
- + flagKey
- + " action assignment for subject "
- + expectedSubjectAssignment.getSubjectKey();
-
- assertEquals(
- expectedSubjectAssignment.getAssignment().getAction(),
- assignment.getAction(),
- failureMessage);
+ public static Stream getBanditTestData() {
+ return BanditTestCase.getBanditTestData();
}
+ @SuppressWarnings("ExtractMethodRecommender")
@Test
public void testBanditLogsAction() {
String flagKey = "banner_bandit_flag";
@@ -165,8 +105,7 @@ public void testBanditLogsAction() {
actions.put("reebok", rebookAttributes);
BanditResult banditResult =
- BaseEppoClient.getInstance()
- .getBanditAction(flagKey, subjectKey, subjectAttributes, actions, "control");
+ eppoClient.getBanditAction(flagKey, subjectKey, subjectAttributes, actions, "control");
// Verify assignment
assertEquals("banner_bandit", banditResult.getVariation());
@@ -239,8 +178,7 @@ public void testNoBanditLogsWhenNotBandit() {
actions.put("adidas", new Attributes());
BanditResult banditResult =
- BaseEppoClient.getInstance()
- .getBanditAction(flagKey, subjectKey, subjectAttributes, actions, "default");
+ eppoClient.getBanditAction(flagKey, subjectKey, subjectAttributes, actions, "default");
// Verify assignment
assertEquals("control", banditResult.getVariation());
@@ -267,8 +205,7 @@ public void testNoBanditLogsWhenNoActions() {
BanditActions actions = new BanditActions();
BanditResult banditResult =
- BaseEppoClient.getInstance()
- .getBanditAction(flagKey, subjectKey, subjectAttributes, actions, "control");
+ eppoClient.getBanditAction(flagKey, subjectKey, subjectAttributes, actions, "control");
// Verify assignment
assertEquals("banner_bandit", banditResult.getVariation());
@@ -286,9 +223,8 @@ public void testNoBanditLogsWhenNoActions() {
@Test
public void testBanditErrorGracefulModeOff() {
- BaseEppoClient.getInstance()
- .setIsGracefulFailureMode(
- false); // Should be set by @BeforeEach but repeated here for test clarity
+ eppoClient.setIsGracefulFailureMode(
+ false); // Should be set by @BeforeEach but repeated here for test clarity
try (MockedStatic mockedStatic = mockStatic(BanditEvaluator.class)) {
// Configure the mock to throw an exception
mockedStatic
@@ -302,15 +238,14 @@ public void testBanditErrorGracefulModeOff() {
assertThrows(
RuntimeException.class,
() ->
- BaseEppoClient.getInstance()
- .getBanditAction(
- "banner_bandit_flag", "subject", new Attributes(), actions, "default"));
+ eppoClient.getBanditAction(
+ "banner_bandit_flag", "subject", new Attributes(), actions, "default"));
}
}
@Test
public void testBanditErrorGracefulModeOn() {
- BaseEppoClient.getInstance().setIsGracefulFailureMode(true);
+ eppoClient.setIsGracefulFailureMode(true);
try (MockedStatic mockedStatic = mockStatic(BanditEvaluator.class)) {
// Configure the mock to throw an exception
mockedStatic
@@ -322,9 +257,8 @@ public void testBanditErrorGracefulModeOn() {
actions.put("nike", new Attributes());
actions.put("adidas", new Attributes());
BanditResult banditResult =
- BaseEppoClient.getInstance()
- .getBanditAction(
- "banner_bandit_flag", "subject", new Attributes(), actions, "default");
+ eppoClient.getBanditAction(
+ "banner_bandit_flag", "subject", new Attributes(), actions, "default");
assertEquals("banner_bandit", banditResult.getVariation());
assertNull(banditResult.getAction());
}
@@ -341,8 +275,8 @@ public void testBanditLogErrorNonFatal() {
actions.put("nike", new Attributes());
actions.put("adidas", new Attributes());
BanditResult banditResult =
- BaseEppoClient.getInstance()
- .getBanditAction("banner_bandit_flag", "subject", new Attributes(), actions, "default");
+ eppoClient.getBanditAction(
+ "banner_bandit_flag", "subject", new Attributes(), actions, "default");
assertEquals("banner_bandit", banditResult.getVariation());
assertEquals("nike", banditResult.getAction());
@@ -350,10 +284,4 @@ public void testBanditLogErrorNonFatal() {
ArgumentCaptor.forClass(BanditAssignment.class);
verify(mockBanditLogger, times(1)).logBanditAssignment(banditLogCaptor.capture());
}
-
- private static SimpleModule module() {
- SimpleModule module = new SimpleModule();
- module.addDeserializer(BanditTestCase.class, new BanditTestCaseDeserializer());
- return module;
- }
}
diff --git a/src/test/java/cloud/eppo/BaseEppoClientTest.java b/src/test/java/cloud/eppo/BaseEppoClientTest.java
index 9b047cc0..80670daf 100644
--- a/src/test/java/cloud/eppo/BaseEppoClientTest.java
+++ b/src/test/java/cloud/eppo/BaseEppoClientTest.java
@@ -1,35 +1,26 @@
package cloud.eppo;
+import static cloud.eppo.helpers.AssignmentTestCase.parseTestCaseFile;
+import static cloud.eppo.helpers.AssignmentTestCase.runTestCase;
+import static cloud.eppo.helpers.TestUtils.mockHttpResponse;
+import static cloud.eppo.helpers.TestUtils.setBaseClientHttpClientOverrideField;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import cloud.eppo.helpers.AssignmentTestCase;
-import cloud.eppo.helpers.AssignmentTestCaseDeserializer;
-import cloud.eppo.helpers.SubjectAssignment;
-import cloud.eppo.helpers.TestCaseValue;
import cloud.eppo.logging.Assignment;
import cloud.eppo.logging.AssignmentLogger;
import cloud.eppo.ufc.dto.Attributes;
import cloud.eppo.ufc.dto.EppoValue;
import cloud.eppo.ufc.dto.VariationType;
import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.File;
-import java.io.IOException;
-import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Stream;
-import okhttp3.*;
-import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -44,185 +35,67 @@ public class BaseEppoClientTest {
private static final String DUMMY_FLAG_API_KEY = "dummy-flags-api-key"; // Will load flags-v1
private static final String TEST_HOST =
"https://us-central1-eppo-qa.cloudfunctions.net/serveGitHubRacTestFile";
- private final ObjectMapper mapper = new ObjectMapper().registerModule(module());
+ private final ObjectMapper mapper =
+ new ObjectMapper().registerModule(AssignmentTestCase.assignmentTestCaseModule());
+ private BaseEppoClient eppoClient;
private AssignmentLogger mockAssignmentLogger;
// TODO: async init client tests
private void initClient() {
- initClient(TEST_HOST, false, false, DUMMY_FLAG_API_KEY);
+ initClient(false, false);
}
- private void initClient(
- String host, boolean isGracefulMode, boolean isConfigObfuscated, String apiKey) {
+ private void initClient(boolean isGracefulMode, boolean isConfigObfuscated) {
mockAssignmentLogger = mock(AssignmentLogger.class);
- new BaseEppoClient.Builder()
- .apiKey(apiKey)
- .sdkName(isConfigObfuscated ? "android" : "java")
- .sdkVersion("3.0.0")
- .isGracefulMode(isGracefulMode)
- .host(host)
- .assignmentLogger(mockAssignmentLogger)
- .buildAndInit();
-
+ eppoClient =
+ new BaseEppoClient(
+ DUMMY_FLAG_API_KEY,
+ isConfigObfuscated ? "android" : "java",
+ "3.0.0",
+ TEST_HOST,
+ mockAssignmentLogger,
+ null,
+ isGracefulMode,
+ isConfigObfuscated);
+
+ eppoClient.loadConfiguration();
log.info("Test client initialized");
}
@BeforeEach
public void cleanUp() {
// TODO: Clear any caches
- setHttpClientOverrideField(null);
+ setBaseClientHttpClientOverrideField(null);
}
@ParameterizedTest
@MethodSource("getAssignmentTestData")
public void testUnobfuscatedAssignments(File testFile) {
- initClient(TEST_HOST, false, false, DUMMY_FLAG_API_KEY);
+ initClient(false, false);
AssignmentTestCase testCase = parseTestCaseFile(testFile);
- runTestCase(testCase);
+ runTestCase(testCase, eppoClient);
}
@ParameterizedTest
@MethodSource("getAssignmentTestData")
public void testObfuscatedAssignments(File testFile) {
- initClient(TEST_HOST, false, true, DUMMY_FLAG_API_KEY);
+ initClient(false, true);
AssignmentTestCase testCase = parseTestCaseFile(testFile);
- runTestCase(testCase);
+ runTestCase(testCase, eppoClient);
}
private static Stream getAssignmentTestData() {
- File testCaseFolder = new File("src/test/resources/shared/ufc/tests");
- File[] testCaseFiles = testCaseFolder.listFiles();
- assertNotNull(testCaseFiles);
- assertTrue(testCaseFiles.length > 0);
- List arguments = new ArrayList<>();
- for (File testCaseFile : testCaseFiles) {
- arguments.add(Arguments.of(testCaseFile));
- }
- return arguments.stream();
- }
-
- private AssignmentTestCase parseTestCaseFile(File testCaseFile) {
- AssignmentTestCase testCase;
- try {
- String json = FileUtils.readFileToString(testCaseFile, "UTF8");
- testCase = mapper.readValue(json, AssignmentTestCase.class);
- } catch (IOException ex) {
- throw new RuntimeException(ex);
- }
- return testCase;
- }
-
- private void runTestCase(AssignmentTestCase testCase) {
- String flagKey = testCase.getFlag();
- TestCaseValue defaultValue = testCase.getDefaultValue();
- BaseEppoClient eppoClient = BaseEppoClient.getInstance();
- assertFalse(testCase.getSubjects().isEmpty());
-
- for (SubjectAssignment subjectAssignment : testCase.getSubjects()) {
- String subjectKey = subjectAssignment.getSubjectKey();
- Attributes subjectAttributes = subjectAssignment.getSubjectAttributes();
-
- // Depending on the variation type, we will need to change which assignment method we call and
- // how we get the default value
- switch (testCase.getVariationType()) {
- case BOOLEAN:
- boolean boolAssignment =
- eppoClient.getBooleanAssignment(
- flagKey, subjectKey, subjectAttributes, defaultValue.booleanValue());
- assertAssignment(flagKey, subjectAssignment, boolAssignment);
- break;
- case INTEGER:
- int intAssignment =
- eppoClient.getIntegerAssignment(
- flagKey,
- subjectKey,
- subjectAttributes,
- Double.valueOf(defaultValue.doubleValue()).intValue());
- assertAssignment(flagKey, subjectAssignment, intAssignment);
- break;
- case NUMERIC:
- double doubleAssignment =
- eppoClient.getDoubleAssignment(
- flagKey, subjectKey, subjectAttributes, defaultValue.doubleValue());
- assertAssignment(flagKey, subjectAssignment, doubleAssignment);
- break;
- case STRING:
- String stringAssignment =
- eppoClient.getStringAssignment(
- flagKey, subjectKey, subjectAttributes, defaultValue.stringValue());
- assertAssignment(flagKey, subjectAssignment, stringAssignment);
- break;
- case JSON:
- JsonNode jsonAssignment =
- eppoClient.getJSONAssignment(
- flagKey, subjectKey, subjectAttributes, testCase.getDefaultValue().jsonValue());
- assertAssignment(flagKey, subjectAssignment, jsonAssignment);
- break;
- default:
- throw new UnsupportedOperationException(
- "Unexpected variation type "
- + testCase.getVariationType()
- + " for "
- + flagKey
- + " test case");
- }
- }
- }
-
- /** Helper method for asserting a subject assignment with a useful failure message. */
- private void assertAssignment(
- String flagKey, SubjectAssignment expectedSubjectAssignment, T assignment) {
-
- if (assignment == null) {
- fail(
- "Unexpected null "
- + flagKey
- + " assignment for subject "
- + expectedSubjectAssignment.getSubjectKey());
- }
-
- String failureMessage =
- "Incorrect "
- + flagKey
- + " assignment for subject "
- + expectedSubjectAssignment.getSubjectKey();
-
- if (assignment instanceof Boolean) {
- assertEquals(
- expectedSubjectAssignment.getAssignment().booleanValue(), assignment, failureMessage);
- } else if (assignment instanceof Integer) {
- assertEquals(
- Double.valueOf(expectedSubjectAssignment.getAssignment().doubleValue()).intValue(),
- assignment,
- failureMessage);
- } else if (assignment instanceof Double) {
- assertEquals(
- expectedSubjectAssignment.getAssignment().doubleValue(),
- (Double) assignment,
- 0.000001,
- failureMessage);
- } else if (assignment instanceof String) {
- assertEquals(
- expectedSubjectAssignment.getAssignment().stringValue(), assignment, failureMessage);
- } else if (assignment instanceof JsonNode) {
- assertEquals(
- expectedSubjectAssignment.getAssignment().jsonValue().toString(),
- assignment.toString(),
- failureMessage);
- } else {
- throw new IllegalArgumentException(
- "Unexpected assignment type " + assignment.getClass().getCanonicalName());
- }
+ return AssignmentTestCase.getAssignmentTestData();
}
@Test
public void testErrorGracefulModeOn() throws JsonProcessingException {
- initClient(TEST_HOST, true, false, DUMMY_FLAG_API_KEY);
+ initClient(true, false);
- BaseEppoClient realClient = BaseEppoClient.getInstance();
+ BaseEppoClient realClient = eppoClient;
BaseEppoClient spyClient = spy(realClient);
doThrow(new RuntimeException("Exception thrown by mock"))
.when(spyClient)
@@ -269,9 +142,9 @@ public void testErrorGracefulModeOn() throws JsonProcessingException {
@Test
public void testErrorGracefulModeOff() {
- initClient(TEST_HOST, false, false, DUMMY_FLAG_API_KEY);
+ initClient(false, false);
- BaseEppoClient realClient = BaseEppoClient.getInstance();
+ BaseEppoClient realClient = eppoClient;
BaseEppoClient spyClient = spy(realClient);
doThrow(new RuntimeException("Exception thrown by mock"))
.when(spyClient)
@@ -325,13 +198,11 @@ public void testErrorGracefulModeOff() {
@Test
public void testInvalidConfigJSON() {
- mockHttpResponse("{}");
+ mockHttpResponse(TEST_HOST, "{}");
- initClient(TEST_HOST, false, false, DUMMY_FLAG_API_KEY);
+ initClient(false, false);
- String result =
- BaseEppoClient.getInstance()
- .getStringAssignment("dummy subject", "dummy flag", "not-populated");
+ String result = eppoClient.getStringAssignment("dummy subject", "dummy flag", "not-populated");
assertEquals("not-populated", result);
}
@@ -343,8 +214,7 @@ public void testAssignmentEventCorrectlyCreated() {
subjectAttributes.put("age", EppoValue.valueOf(30));
subjectAttributes.put("employer", EppoValue.valueOf("Eppo"));
double assignment =
- BaseEppoClient.getInstance()
- .getDoubleAssignment("numeric_flag", "alice", subjectAttributes, 0.0);
+ eppoClient.getDoubleAssignment("numeric_flag", "alice", subjectAttributes, 0.0);
assertEquals(3.1415926, assignment, 0.0000001);
@@ -381,8 +251,7 @@ public void testAssignmentLogErrorNonFatal() {
.when(mockAssignmentLogger)
.logAssignment(any());
double assignment =
- BaseEppoClient.getInstance()
- .getDoubleAssignment("numeric_flag", "alice", new Attributes(), 0.0);
+ eppoClient.getDoubleAssignment("numeric_flag", "alice", new Attributes(), 0.0);
assertEquals(3.1415926, assignment, 0.0000001);
@@ -390,61 +259,5 @@ public void testAssignmentLogErrorNonFatal() {
verify(mockAssignmentLogger, times(1)).logAssignment(assignmentLogCaptor.capture());
}
- private void mockHttpResponse(String responseBody) {
- // Create a mock instance of EppoHttpClient
- EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);
-
- // Mock sync get
- Response dummyResponse =
- new Response.Builder()
- // Used by test
- .code(200)
- .body(ResponseBody.create(responseBody, MediaType.get("application/json")))
- // Below properties are required to build the Response (but unused)
- .request(new Request.Builder().url(TEST_HOST).build())
- .protocol(Protocol.HTTP_1_1)
- .message("OK")
- .build();
- when(mockHttpClient.get(anyString())).thenReturn(dummyResponse);
-
- // Mock async get
- doAnswer(
- invocation -> {
- RequestCallback callback = invocation.getArgument(1);
- callback.onSuccess(responseBody);
- return null; // doAnswer doesn't require a return value
- })
- .when(mockHttpClient)
- .get(anyString(), any(RequestCallback.class));
-
- setHttpClientOverrideField(mockHttpClient);
- }
-
- private void setHttpClientOverrideField(EppoHttpClient httpClient) {
- setOverrideField("httpClientOverride", httpClient);
- }
-
- private void setConfigurationStoreOverrideField(ConfigurationStore configurationStore) {
- setOverrideField("configurationStoreOverride", configurationStore);
- }
-
- /** Uses reflection to set a static override field used for tests (e.g., httpClientOverride) */
- private void setOverrideField(String fieldName, T override) {
- try {
- Field httpClientOverrideField = BaseEppoClient.class.getDeclaredField(fieldName);
- httpClientOverrideField.setAccessible(true);
- httpClientOverrideField.set(null, override);
- httpClientOverrideField.setAccessible(false);
- } catch (NoSuchFieldException | IllegalAccessException e) {
- throw new RuntimeException(e);
- }
- }
-
- private static SimpleModule module() {
- SimpleModule module = new SimpleModule();
- module.addDeserializer(AssignmentTestCase.class, new AssignmentTestCaseDeserializer());
- return module;
- }
-
// TODO: tests for the cache
}
diff --git a/src/test/java/cloud/eppo/FlagEvaluatorTest.java b/src/test/java/cloud/eppo/FlagEvaluatorTest.java
index c8e2dd84..d5effda1 100644
--- a/src/test/java/cloud/eppo/FlagEvaluatorTest.java
+++ b/src/test/java/cloud/eppo/FlagEvaluatorTest.java
@@ -3,7 +3,6 @@
import static cloud.eppo.Utils.base64Encode;
import static cloud.eppo.Utils.getMD5Hex;
import static org.junit.jupiter.api.Assertions.*;
-import static org.junit.jupiter.api.Assertions.assertEquals;
import cloud.eppo.model.ShardRange;
import cloud.eppo.ufc.dto.Allocation;
diff --git a/src/test/java/cloud/eppo/helpers/AssignmentTestCase.java b/src/test/java/cloud/eppo/helpers/AssignmentTestCase.java
index 7c2e9ea0..4513f359 100644
--- a/src/test/java/cloud/eppo/helpers/AssignmentTestCase.java
+++ b/src/test/java/cloud/eppo/helpers/AssignmentTestCase.java
@@ -1,14 +1,26 @@
package cloud.eppo.helpers;
+import static org.junit.jupiter.api.Assertions.*;
+
+import cloud.eppo.BaseEppoClient;
+import cloud.eppo.ufc.dto.Attributes;
import cloud.eppo.ufc.dto.VariationType;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
+import java.util.stream.Stream;
+import org.apache.commons.io.FileUtils;
+import org.junit.jupiter.params.provider.Arguments;
public class AssignmentTestCase {
private final String flag;
private final VariationType variationType;
private final TestCaseValue defaultValue;
private final List subjects;
- private String fileName;
public AssignmentTestCase(
String flag,
@@ -37,11 +49,138 @@ public List getSubjects() {
return subjects;
}
- public void setFileName(String fileName) {
- this.fileName = fileName;
+ private static final ObjectMapper mapper =
+ new ObjectMapper().registerModule(assignmentTestCaseModule());
+
+ public static SimpleModule assignmentTestCaseModule() {
+ SimpleModule module = new SimpleModule();
+ module.addDeserializer(AssignmentTestCase.class, new AssignmentTestCaseDeserializer());
+ return module;
+ }
+
+ public static Stream getAssignmentTestData() {
+ File testCaseFolder = new File("src/test/resources/shared/ufc/tests");
+ File[] testCaseFiles = testCaseFolder.listFiles();
+ assertNotNull(testCaseFiles);
+ assertTrue(testCaseFiles.length > 0);
+ List arguments = new ArrayList<>();
+ for (File testCaseFile : testCaseFiles) {
+ arguments.add(Arguments.of(testCaseFile));
+ }
+ return arguments.stream();
+ }
+
+ public static AssignmentTestCase parseTestCaseFile(File testCaseFile) {
+ AssignmentTestCase testCase;
+ try {
+ String json = FileUtils.readFileToString(testCaseFile, "UTF8");
+
+ testCase = mapper.readValue(json, AssignmentTestCase.class);
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ return testCase;
+ }
+
+ public static void runTestCase(AssignmentTestCase testCase, BaseEppoClient eppoClient) {
+ String flagKey = testCase.getFlag();
+ TestCaseValue defaultValue = testCase.getDefaultValue();
+ assertFalse(testCase.getSubjects().isEmpty());
+
+ for (SubjectAssignment subjectAssignment : testCase.getSubjects()) {
+ String subjectKey = subjectAssignment.getSubjectKey();
+ Attributes subjectAttributes = subjectAssignment.getSubjectAttributes();
+
+ // Depending on the variation type, we will need to change which assignment method we call and
+ // how we get the default value
+ switch (testCase.getVariationType()) {
+ case BOOLEAN:
+ boolean boolAssignment =
+ eppoClient.getBooleanAssignment(
+ flagKey, subjectKey, subjectAttributes, defaultValue.booleanValue());
+ assertAssignment(flagKey, subjectAssignment, boolAssignment);
+ break;
+ case INTEGER:
+ int intAssignment =
+ eppoClient.getIntegerAssignment(
+ flagKey,
+ subjectKey,
+ subjectAttributes,
+ Double.valueOf(defaultValue.doubleValue()).intValue());
+ assertAssignment(flagKey, subjectAssignment, intAssignment);
+ break;
+ case NUMERIC:
+ double doubleAssignment =
+ eppoClient.getDoubleAssignment(
+ flagKey, subjectKey, subjectAttributes, defaultValue.doubleValue());
+ assertAssignment(flagKey, subjectAssignment, doubleAssignment);
+ break;
+ case STRING:
+ String stringAssignment =
+ eppoClient.getStringAssignment(
+ flagKey, subjectKey, subjectAttributes, defaultValue.stringValue());
+ assertAssignment(flagKey, subjectAssignment, stringAssignment);
+ break;
+ case JSON:
+ JsonNode jsonAssignment =
+ eppoClient.getJSONAssignment(
+ flagKey, subjectKey, subjectAttributes, testCase.getDefaultValue().jsonValue());
+ assertAssignment(flagKey, subjectAssignment, jsonAssignment);
+ break;
+ default:
+ throw new UnsupportedOperationException(
+ "Unexpected variation type "
+ + testCase.getVariationType()
+ + " for "
+ + flagKey
+ + " test case");
+ }
+ }
}
- public String getFileName() {
- return fileName;
+ /** Helper method for asserting a subject assignment with a useful failure message. */
+ private static void assertAssignment(
+ String flagKey, SubjectAssignment expectedSubjectAssignment, T assignment) {
+
+ if (assignment == null) {
+ fail(
+ "Unexpected null "
+ + flagKey
+ + " assignment for subject "
+ + expectedSubjectAssignment.getSubjectKey());
+ }
+
+ String failureMessage =
+ "Incorrect "
+ + flagKey
+ + " assignment for subject "
+ + expectedSubjectAssignment.getSubjectKey();
+
+ if (assignment instanceof Boolean) {
+ assertEquals(
+ expectedSubjectAssignment.getAssignment().booleanValue(), assignment, failureMessage);
+ } else if (assignment instanceof Integer) {
+ assertEquals(
+ Double.valueOf(expectedSubjectAssignment.getAssignment().doubleValue()).intValue(),
+ assignment,
+ failureMessage);
+ } else if (assignment instanceof Double) {
+ assertEquals(
+ expectedSubjectAssignment.getAssignment().doubleValue(),
+ (Double) assignment,
+ 0.000001,
+ failureMessage);
+ } else if (assignment instanceof String) {
+ assertEquals(
+ expectedSubjectAssignment.getAssignment().stringValue(), assignment, failureMessage);
+ } else if (assignment instanceof JsonNode) {
+ assertEquals(
+ expectedSubjectAssignment.getAssignment().jsonValue().toString(),
+ assignment.toString(),
+ failureMessage);
+ } else {
+ throw new IllegalArgumentException(
+ "Unexpected assignment type " + assignment.getClass().getCanonicalName());
+ }
}
}
diff --git a/src/test/java/cloud/eppo/helpers/BanditTestCase.java b/src/test/java/cloud/eppo/helpers/BanditTestCase.java
index 90e6ff86..5c6efe67 100644
--- a/src/test/java/cloud/eppo/helpers/BanditTestCase.java
+++ b/src/test/java/cloud/eppo/helpers/BanditTestCase.java
@@ -1,6 +1,21 @@
package cloud.eppo.helpers;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import cloud.eppo.BaseEppoClient;
+import cloud.eppo.ufc.dto.Actions;
+import cloud.eppo.ufc.dto.BanditResult;
+import cloud.eppo.ufc.dto.ContextAttributes;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
+import java.util.stream.Stream;
+import org.apache.commons.io.FileUtils;
+import org.junit.jupiter.params.provider.Arguments;
public class BanditTestCase {
private final String flag;
@@ -26,11 +41,77 @@ public List getSubjects() {
return subjects;
}
- public void setFileName(String fileName) {
- this.fileName = fileName;
+ public static Stream getBanditTestData() {
+ File testCaseFolder = new File("src/test/resources/shared/ufc/bandit-tests");
+ File[] testCaseFiles = testCaseFolder.listFiles();
+ assertNotNull(testCaseFiles);
+ assertTrue(testCaseFiles.length > 0);
+ List arguments = new ArrayList<>();
+ for (File testCaseFile : testCaseFiles) {
+ arguments.add(Arguments.of(testCaseFile));
+ }
+ return arguments.stream();
+ }
+
+ private static final ObjectMapper mapper =
+ new ObjectMapper().registerModule(banditTestCaseModule());
+
+ public static SimpleModule banditTestCaseModule() {
+ SimpleModule module = new SimpleModule();
+ module.addDeserializer(BanditTestCase.class, new BanditTestCaseDeserializer());
+ return module;
}
- public String getFileName() {
- return fileName;
+ public static BanditTestCase parseBanditTestCaseFile(File testCaseFile) {
+ BanditTestCase testCase;
+ try {
+ String json = FileUtils.readFileToString(testCaseFile, "UTF8");
+ testCase = mapper.readValue(json, BanditTestCase.class);
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ return testCase;
+ }
+
+ public static void runBanditTestCase(BanditTestCase testCase, BaseEppoClient eppoClient) {
+ assertFalse(testCase.getSubjects().isEmpty());
+
+ String flagKey = testCase.getFlag();
+ String defaultValue = testCase.getDefaultValue();
+
+ for (BanditSubjectAssignment subjectAssignment : testCase.getSubjects()) {
+ String subjectKey = subjectAssignment.getSubjectKey();
+ ContextAttributes attributes = subjectAssignment.getSubjectAttributes();
+ Actions actions = subjectAssignment.getActions();
+ BanditResult assignment =
+ eppoClient.getBanditAction(flagKey, subjectKey, attributes, actions, defaultValue);
+ assertBanditAssignment(flagKey, subjectAssignment, assignment);
+ }
+ }
+
+ /** Helper method for asserting a bandit assignment with a useful failure message. */
+ private static void assertBanditAssignment(
+ String flagKey, BanditSubjectAssignment expectedSubjectAssignment, BanditResult assignment) {
+ String failureMessage =
+ "Incorrect "
+ + flagKey
+ + " variation assignment for subject "
+ + expectedSubjectAssignment.getSubjectKey();
+
+ assertEquals(
+ expectedSubjectAssignment.getAssignment().getVariation(),
+ assignment.getVariation(),
+ failureMessage);
+
+ failureMessage =
+ "Incorrect "
+ + flagKey
+ + " action assignment for subject "
+ + expectedSubjectAssignment.getSubjectKey();
+
+ assertEquals(
+ expectedSubjectAssignment.getAssignment().getAction(),
+ assignment.getAction(),
+ failureMessage);
}
}
diff --git a/src/test/java/cloud/eppo/helpers/TestUtils.java b/src/test/java/cloud/eppo/helpers/TestUtils.java
new file mode 100644
index 00000000..ab0c65e6
--- /dev/null
+++ b/src/test/java/cloud/eppo/helpers/TestUtils.java
@@ -0,0 +1,62 @@
+package cloud.eppo.helpers;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+
+import cloud.eppo.BaseEppoClient;
+import cloud.eppo.EppoHttpClient;
+import cloud.eppo.EppoHttpClientRequestCallback;
+import java.lang.reflect.Field;
+import okhttp3.*;
+
+public class TestUtils {
+
+ @SuppressWarnings("SameParameterValue")
+ public static void mockHttpResponse(String host, String responseBody) {
+ // Create a mock instance of EppoHttpClient
+ EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);
+
+ // Mock sync get
+ Response dummyResponse =
+ new Response.Builder()
+ // Used by test
+ .code(200)
+ .body(ResponseBody.create(responseBody, MediaType.get("application/json")))
+ // Below properties are required to build the Response (but unused)
+ .request(new Request.Builder().url(host).build())
+ .protocol(Protocol.HTTP_1_1)
+ .message("OK")
+ .build();
+ when(mockHttpClient.get(anyString())).thenReturn(dummyResponse);
+
+ // Mock async get
+ doAnswer(
+ invocation -> {
+ EppoHttpClientRequestCallback callback = invocation.getArgument(1);
+ callback.onSuccess(responseBody);
+ return null; // doAnswer doesn't require a return value
+ })
+ .when(mockHttpClient)
+ .get(anyString(), any(EppoHttpClientRequestCallback.class));
+
+ setBaseClientHttpClientOverrideField(mockHttpClient);
+ }
+
+ public static void setBaseClientHttpClientOverrideField(EppoHttpClient httpClient) {
+ setBaseClientOverrideField("httpClientOverride", httpClient);
+ }
+
+ /** Uses reflection to set a static override field used for tests (e.g., httpClientOverride) */
+ @SuppressWarnings("SameParameterValue")
+ public static void setBaseClientOverrideField(String fieldName, T override) {
+ try {
+ Field httpClientOverrideField = BaseEppoClient.class.getDeclaredField(fieldName);
+ httpClientOverrideField.setAccessible(true);
+ httpClientOverrideField.set(null, override);
+ httpClientOverrideField.setAccessible(false);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}