diff --git a/src/main/java/com/devcycle/sdk/server/cloud/api/DevCycleCloudClient.java b/src/main/java/com/devcycle/sdk/server/cloud/api/DevCycleCloudClient.java index 9f9297a3..00419714 100755 --- a/src/main/java/com/devcycle/sdk/server/cloud/api/DevCycleCloudClient.java +++ b/src/main/java/com/devcycle/sdk/server/cloud/api/DevCycleCloudClient.java @@ -51,7 +51,7 @@ public DevCycleCloudClient(String sdkKey, DevCycleCloudOptions options) { OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); this.openFeatureProvider = new DevCycleProvider(this); - this.evalHooksRunner = new EvalHooksRunner(); + this.evalHooksRunner = new EvalHooksRunner(dvcOptions.getHooks()); } /** diff --git a/src/main/java/com/devcycle/sdk/server/cloud/model/DevCycleCloudOptions.java b/src/main/java/com/devcycle/sdk/server/cloud/model/DevCycleCloudOptions.java index 3c31c15d..2664cf13 100644 --- a/src/main/java/com/devcycle/sdk/server/cloud/model/DevCycleCloudOptions.java +++ b/src/main/java/com/devcycle/sdk/server/cloud/model/DevCycleCloudOptions.java @@ -2,10 +2,14 @@ import com.devcycle.sdk.server.common.api.IRestOptions; import com.devcycle.sdk.server.common.logging.IDevCycleLogger; +import com.devcycle.sdk.server.common.model.EvalHook; import com.devcycle.sdk.server.common.model.IDevCycleOptions; import lombok.Builder; import lombok.Data; +import java.util.ArrayList; +import java.util.List; + @Data @Builder public class DevCycleCloudOptions { @@ -21,6 +25,9 @@ public class DevCycleCloudOptions { @Builder.Default private IRestOptions restOptions = null; + @Builder.Default + private List hooks = new ArrayList<>(); + public static class DevCycleCloudOptionsBuilder implements IDevCycleOptions { } } diff --git a/src/main/java/com/devcycle/sdk/server/common/model/EvalHooksRunner.java b/src/main/java/com/devcycle/sdk/server/common/model/EvalHooksRunner.java index 3a25d6e0..d7342782 100644 --- a/src/main/java/com/devcycle/sdk/server/common/model/EvalHooksRunner.java +++ b/src/main/java/com/devcycle/sdk/server/common/model/EvalHooksRunner.java @@ -18,6 +18,14 @@ public class EvalHooksRunner { /** * Default constructor initializes an empty list of hooks. */ + public EvalHooksRunner(List> hooks) { + if (hooks == null) { + this.hooks = new ArrayList<>(); + } else { + this.hooks = hooks; + } + } + public EvalHooksRunner() { this.hooks = new ArrayList<>(); } diff --git a/src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalClient.java b/src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalClient.java index 094be70b..236967ab 100755 --- a/src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalClient.java +++ b/src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalClient.java @@ -60,7 +60,7 @@ public DevCycleLocalClient(String sdkKey, DevCycleLocalOptions dvcOptions) { } catch (Exception e) { DevCycleLogger.error("Error creating event queue due to error: " + e.getMessage()); } - this.evalHooksRunner = new EvalHooksRunner(); + this.evalHooksRunner = new EvalHooksRunner(dvcOptions.getHooks()); } /** diff --git a/src/main/java/com/devcycle/sdk/server/local/model/DevCycleLocalOptions.java b/src/main/java/com/devcycle/sdk/server/local/model/DevCycleLocalOptions.java index 6e13b839..77c39d39 100644 --- a/src/main/java/com/devcycle/sdk/server/local/model/DevCycleLocalOptions.java +++ b/src/main/java/com/devcycle/sdk/server/local/model/DevCycleLocalOptions.java @@ -3,11 +3,15 @@ import com.devcycle.sdk.server.common.api.IRestOptions; import com.devcycle.sdk.server.common.logging.DevCycleLogger; import com.devcycle.sdk.server.common.logging.IDevCycleLogger; +import com.devcycle.sdk.server.common.model.EvalHook; import com.devcycle.sdk.server.common.model.IDevCycleOptions; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Builder; import lombok.Data; +import java.util.ArrayList; +import java.util.List; + @Data public class DevCycleLocalOptions implements IDevCycleOptions { private int configRequestTimeoutMs = 10000; @@ -41,6 +45,8 @@ public class DevCycleLocalOptions implements IDevCycleOptions { private boolean disableCustomEventLogging = false; @JsonIgnore private IRestOptions restOptions = null; + @JsonIgnore + private List hooks = new ArrayList<>(); @Builder() public DevCycleLocalOptions( @@ -60,7 +66,8 @@ public DevCycleLocalOptions( IRestOptions restOptions, @Deprecated boolean enableBetaRealtimeUpdates, - boolean disableRealtimeUpdates + boolean disableRealtimeUpdates, + List hooks ) { this.configRequestTimeoutMs = configRequestTimeoutMs > 0 ? configRequestTimeoutMs : this.configRequestTimeoutMs; this.configPollingIntervalMS = getConfigPollingIntervalMS(configPollingIntervalMs, configPollingIntervalMS); @@ -76,6 +83,7 @@ public DevCycleLocalOptions( this.restOptions = restOptions; this.enableBetaRealtimeUpdates = enableBetaRealtimeUpdates; this.disableRealtimeUpdates = disableRealtimeUpdates; + this.hooks = hooks; if (this.flushEventQueueSize >= this.maxEventQueueSize) { DevCycleLogger.warning("flushEventQueueSize: " + this.flushEventQueueSize + " must be smaller than maxEventQueueSize: " + this.maxEventQueueSize); diff --git a/src/test/java/com/devcycle/sdk/server/cloud/DevCycleCloudClientTest.java b/src/test/java/com/devcycle/sdk/server/cloud/DevCycleCloudClientTest.java index 1001ba2e..307ac0da 100755 --- a/src/test/java/com/devcycle/sdk/server/cloud/DevCycleCloudClientTest.java +++ b/src/test/java/com/devcycle/sdk/server/cloud/DevCycleCloudClientTest.java @@ -8,6 +8,7 @@ import com.devcycle.sdk.server.common.model.*; import com.devcycle.sdk.server.helpers.WhiteBox; import dev.openfeature.sdk.Hook; +import net.bytebuddy.implementation.bytecode.Throw; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -23,6 +24,7 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.*; import static org.mockito.Mockito.when; @@ -747,6 +749,96 @@ public void onFinally(HookContext ctx, Optional> variab Assert.assertTrue(finallyCalled[0]); } + @Test + public void client_withHooksInOptions_addsHooksToEvalRunner() { + DevCycleUser user = DevCycleUser.builder() + .userId("j_test") + .build(); + + final boolean[] beforeCalled = {false, false}; + final boolean[] afterCalled = {false, false}; + final boolean[] finallyCalled = {false, false}; + final boolean[] errorCalled = {false, false}; + + List hooks = new ArrayList<>(); + hooks.add(new EvalHook() { + @Override + public Optional> before(HookContext ctx) { + beforeCalled[0] = true; + return Optional.empty(); + } + + @Override + public void after(HookContext ctx, Variable variable) { + afterCalled[0] = true; + } + + @Override + public void onFinally(HookContext ctx, Optional> variable) { + finallyCalled[0] = true; + } + + @Override + public void error(HookContext ctx, Throwable e) { + errorCalled[0] = true; + } + }); + hooks.add(new EvalHook() { + @Override + public Optional> before(HookContext ctx) { + beforeCalled[1] = true; + return Optional.empty(); + } + + @Override + public void after(HookContext ctx, Variable variable) { + afterCalled[1] = true; + } + + @Override + public void onFinally(HookContext ctx, Optional> variable) { + finallyCalled[1] = true; + } + + @Override + public void error(HookContext ctx, Throwable e) { + errorCalled[1] = true; + } + }); + + DevCycleCloudOptions options = DevCycleCloudOptions.builder() + .hooks(hooks) + .build(); + + final String apiKey = String.format("server-%s", UUID.randomUUID()); + DevCycleCloudClient client = new DevCycleCloudClient(apiKey, options); + WhiteBox.setInternalState(client, "api", apiInterface); + + Variable expected = Variable.builder() + .key("test-string") + .value("test value") + .type(Variable.TypeEnum.STRING) + .isDefaulted(false) + .defaultValue("default string") + .build(); + + when(apiInterface.getVariableByKey(user, "test-string", false)).thenReturn(Calls.response(expected)); + + Variable result = client.variable(user, "test-string", "default string"); + + Assert.assertEquals(expected, result); + + Assert.assertTrue(beforeCalled[0]); + Assert.assertTrue(afterCalled[0]); + Assert.assertTrue(finallyCalled[0]); + Assert.assertFalse(errorCalled[0]); + + Assert.assertTrue(beforeCalled[1]); + Assert.assertTrue(afterCalled[1]); + Assert.assertTrue(finallyCalled[1]); + Assert.assertFalse(errorCalled[1]); + } + private void assertUserDefaultsCorrect(DevCycleUser user) { Assert.assertEquals("Java", user.getPlatform()); Assert.assertEquals(PlatformData.SdkTypeEnum.SERVER, user.getSdkType()); diff --git a/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java b/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java index 0cbc27c6..a098812e 100644 --- a/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java +++ b/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java @@ -1,11 +1,14 @@ package com.devcycle.sdk.server.local; +import com.devcycle.sdk.server.cloud.api.DevCycleCloudClient; +import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions; import com.devcycle.sdk.server.common.api.IRestOptions; import com.devcycle.sdk.server.common.exception.DevCycleException; import com.devcycle.sdk.server.common.logging.IDevCycleLogger; import com.devcycle.sdk.server.common.model.*; import com.devcycle.sdk.server.helpers.LocalConfigServer; import com.devcycle.sdk.server.helpers.TestDataFixtures; +import com.devcycle.sdk.server.helpers.WhiteBox; import com.devcycle.sdk.server.local.api.DevCycleLocalClient; import com.devcycle.sdk.server.local.model.DevCycleLocalOptions; import org.junit.AfterClass; @@ -15,6 +18,7 @@ import org.junit.After; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; +import retrofit2.mock.Calls; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; @@ -24,6 +28,9 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.*; + +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class DevCycleLocalClientTest { @@ -90,17 +97,7 @@ public static void setup() throws Exception { client = createClient(TestDataFixtures.SmallConfig()); } - private static DevCycleLocalClient createClient(String config) { - localConfigServer.setConfigData(config); - - DevCycleLocalOptions options = DevCycleLocalOptions.builder() - .configCdnBaseUrl(localConfigServer.getHostRootURL()) - .configPollingIntervalMS(60000) - .customLogger(testLoggingWrapper) - .restOptions(restOptions) - .build(); - - DevCycleLocalClient client = new DevCycleLocalClient(apiKey, options); + private static void waitForClient(DevCycleLocalClient client) { try { int loops = 0; while (!client.isInitialized()) { @@ -115,6 +112,21 @@ private static DevCycleLocalClient createClient(String config) { } catch (InterruptedException e) { // no-op } + } + + private static DevCycleLocalClient createClient(String config) { + localConfigServer.setConfigData(config); + + DevCycleLocalOptions options = DevCycleLocalOptions.builder() + .configCdnBaseUrl(localConfigServer.getHostRootURL()) + .configPollingIntervalMS(60000) + .customLogger(testLoggingWrapper) + .restOptions(restOptions) + .build(); + + DevCycleLocalClient client = new DevCycleLocalClient(apiKey, options); + + waitForClient(client); return client; } @@ -844,6 +856,98 @@ public void onFinally(HookContext ctx, Optional> variab Assert.assertTrue(finallyCalled[0]); } + @Test + public void client_withHooksInOptions_addsHooksToEvalRunner() { + DevCycleUser user = DevCycleUser.builder() + .userId("j_test") + .build(); + + final boolean[] beforeCalled = {false, false}; + final boolean[] afterCalled = {false, false}; + final boolean[] finallyCalled = {false, false}; + final boolean[] errorCalled = {false, false}; + + List hooks = new ArrayList<>(); + hooks.add(new EvalHook() { + @Override + public Optional> before(HookContext ctx) { + beforeCalled[0] = true; + return Optional.empty(); + } + + @Override + public void after(HookContext ctx, Variable variable) { + afterCalled[0] = true; + } + + @Override + public void onFinally(HookContext ctx, Optional> variable) { + finallyCalled[0] = true; + } + + @Override + public void error(HookContext ctx, Throwable e) { + errorCalled[0] = true; + } + }); + hooks.add(new EvalHook() { + @Override + public Optional> before(HookContext ctx) { + beforeCalled[1] = true; + return Optional.empty(); + } + + @Override + public void after(HookContext ctx, Variable variable) { + afterCalled[1] = true; + } + + @Override + public void onFinally(HookContext ctx, Optional> variable) { + finallyCalled[1] = true; + } + + @Override + public void error(HookContext ctx, Throwable e) { + errorCalled[1] = true; + } + }); + + DevCycleLocalOptions options = DevCycleLocalOptions.builder() + .configCdnBaseUrl(localConfigServer.getHostRootURL()) + .configPollingIntervalMS(60000) + .customLogger(testLoggingWrapper) + .restOptions(restOptions) + .hooks(hooks) + .build(); + localConfigServer.setConfigData(TestDataFixtures.SmallConfig()); + + DevCycleLocalClient client = new DevCycleLocalClient(apiKey, options); + waitForClient(client); + + Variable expected = Variable.builder() + .key("string-var") + .value("variationOn") + .type(Variable.TypeEnum.STRING) + .isDefaulted(false) + .defaultValue("default string") + .build(); + + Variable result = client.variable(user, "string-var", "default string"); + + Assert.assertEquals(expected, result); + + Assert.assertTrue(beforeCalled[0]); + Assert.assertTrue(afterCalled[0]); + Assert.assertTrue(finallyCalled[0]); + Assert.assertFalse(errorCalled[0]); + + Assert.assertTrue(beforeCalled[1]); + Assert.assertTrue(afterCalled[1]); + Assert.assertTrue(finallyCalled[1]); + Assert.assertFalse(errorCalled[1]); + } + @After public void clearHooks() { client.clearHooks();