From 5058f56d3d4561a71a242910f87cc9dbbada8041 Mon Sep 17 00:00:00 2001 From: Jamie Sinn Date: Wed, 18 Feb 2026 11:36:54 -0500 Subject: [PATCH 1/2] Allow proxy settings to be overridden. --- .../sdk/server/common/api/APIUtils.java | 10 ++ .../sdk/server/common/api/IRestOptions.java | 19 +++ .../server/local/DevCycleLocalClientTest.java | 130 +++++++++--------- 3 files changed, 91 insertions(+), 68 deletions(-) diff --git a/src/main/java/com/devcycle/sdk/server/common/api/APIUtils.java b/src/main/java/com/devcycle/sdk/server/common/api/APIUtils.java index 536860a7..cdd3c841 100644 --- a/src/main/java/com/devcycle/sdk/server/common/api/APIUtils.java +++ b/src/main/java/com/devcycle/sdk/server/common/api/APIUtils.java @@ -10,6 +10,16 @@ public static void applyRestOptions(IRestOptions restOptions, OkHttpClient.Build builder.hostnameVerifier(restOptions.getHostnameVerifier()); } + if (restOptions.getProxy() != null) { + builder.proxy(restOptions.getProxy()); + if (restOptions.getProxyAuthenticator() != null) { + builder.proxyAuthenticator(restOptions.getProxyAuthenticator()); + } + if (restOptions.getProxySelector() != null) { + builder.proxySelector(restOptions.getProxySelector()); + } + } + if (restOptions.getSocketFactory() != null && restOptions.getTrustManager() != null) { builder.sslSocketFactory(restOptions.getSocketFactory(), restOptions.getTrustManager()); } diff --git a/src/main/java/com/devcycle/sdk/server/common/api/IRestOptions.java b/src/main/java/com/devcycle/sdk/server/common/api/IRestOptions.java index ccc444df..2e612719 100644 --- a/src/main/java/com/devcycle/sdk/server/common/api/IRestOptions.java +++ b/src/main/java/com/devcycle/sdk/server/common/api/IRestOptions.java @@ -1,8 +1,12 @@ package com.devcycle.sdk.server.common.api; +import okhttp3.Authenticator; + import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; +import java.net.Proxy; +import java.net.ProxySelector; import java.util.Map; /** @@ -29,4 +33,19 @@ public interface IRestOptions { * @return A custom HostnameVerifier to use when making requests. Return null if the default HostnameVerifier can be used */ HostnameVerifier getHostnameVerifier(); + + /** + * @return a Proxy to use when making requests. Return null if the default Proxy selector can be used + */ + Proxy getProxy(); + + /** + * @return a ProxySelector to use when making requests. Return null if the default Proxy selector can be used. Requires the proxy() method to be implemented as well. + */ + ProxySelector getProxySelector(); + + /** + * @return an Authenticator to use when making requests. Return null if the default Authenticator can be used. Requires the proxy() method to be implemented as well. + */ + Authenticator getProxyAuthenticator(); } 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 204960a6..981cbfe8 100644 --- a/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java +++ b/src/test/java/com/devcycle/sdk/server/local/DevCycleLocalClientTest.java @@ -1,46 +1,25 @@ package com.devcycle.sdk.server.local; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; - -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - 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.BaseVariable; -import com.devcycle.sdk.server.common.model.DevCycleEvent; -import com.devcycle.sdk.server.common.model.DevCycleUser; -import com.devcycle.sdk.server.common.model.EvalHook; -import com.devcycle.sdk.server.common.model.EvalReason; -import com.devcycle.sdk.server.common.model.Feature; -import com.devcycle.sdk.server.common.model.HookContext; -import com.devcycle.sdk.server.common.model.Variable; +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.local.api.DevCycleLocalClient; -import com.devcycle.sdk.server.local.model.ConfigMetadata; -import com.devcycle.sdk.server.local.model.DevCycleLocalOptions; -import com.devcycle.sdk.server.local.model.Environment; -import com.devcycle.sdk.server.local.model.EnvironmentMetadata; -import com.devcycle.sdk.server.local.model.Project; -import com.devcycle.sdk.server.local.model.ProjectMetadata; -import com.devcycle.sdk.server.local.model.VariableMetadata; +import com.devcycle.sdk.server.local.model.*; +import okhttp3.Authenticator; +import org.junit.*; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; +import java.math.BigDecimal; +import java.net.Proxy; +import java.net.ProxySelector; +import java.util.*; @RunWith(MockitoJUnitRunner.class) public class DevCycleLocalClientTest { @@ -95,6 +74,21 @@ public X509TrustManager getTrustManager() { public HostnameVerifier getHostnameVerifier() { return null; } + + @Override + public Proxy getProxy() { + return null; + } + + @Override + public ProxySelector getProxySelector() { + return null; + } + + @Override + public Authenticator getProxyAuthenticator() { + return null; + } }; private static DevCycleLocalClient client; private static LocalConfigServer localConfigServer; @@ -849,7 +843,7 @@ public Optional> before(HookContext ctx) { } @Override - public void after(HookContext ctx, Variable variable, VariableMetadata variableMetadata) { + public void after(HookContext ctx, Variable variable, VariableMetadata variableMetadata) { afterCalled[0] = true; } @@ -1035,19 +1029,19 @@ public void variable_withEvalHooks_metadataIsAccessibleInAfterHook() throws DevC public void after(HookContext ctx, Variable variable, VariableMetadata variableMetadata) { // Verify metadata is accessible and properly populated Assert.assertNotNull("Metadata should not be null", ctx.getMetadata()); - + // Check that config metadata has the expected structure ConfigMetadata metadata = ctx.getMetadata(); Assert.assertNotNull("Project metadata should not be null", metadata.project); Assert.assertNotNull("Environment metadata should not be null", metadata.environment); - - + + metadataChecked[0] = true; } }); Variable result = client.variable(user, "string-var", "default string"); - + Assert.assertTrue("Metadata check should have been executed", metadataChecked[0]); Assert.assertNotNull("Variable should not be null", result); } @@ -1079,16 +1073,16 @@ public void onFinally(HookContext ctx, Optional> variab }); Variable result = client.variable(user, "string-var", "default string"); - + // Verify all hook stages received metadata Assert.assertNotNull("Before hook should have metadata", capturedMetadata[0]); Assert.assertNotNull("After hook should have metadata", capturedMetadata[1]); Assert.assertNotNull("Finally hook should have metadata", capturedMetadata[2]); - + // Verify metadata is consistent across all hook stages - Assert.assertEquals("Before and after metadata should be the same", + Assert.assertEquals("Before and after metadata should be the same", capturedMetadata[0], capturedMetadata[1]); - Assert.assertEquals("Before and finally metadata should be the same", + Assert.assertEquals("Before and finally metadata should be the same", capturedMetadata[0], capturedMetadata[2]); } @@ -1118,7 +1112,7 @@ public void error(HookContext ctx, Throwable error) { }); Variable result = client.variable(user, "string-var", "default string"); - + Assert.assertTrue("Metadata should have been checked in error hook", metadataCheckedInError[0]); Assert.assertNotNull("Variable should not be null even after error", result); } @@ -1139,16 +1133,16 @@ public void after(HookContext ctx, Variable variable, VariableMe }); Variable result = client.variable(user, "string-var", "default string"); - + Assert.assertNotNull("Metadata should be captured", capturedMetadata[0]); - + // Verify metadata reflects current config state ConfigMetadata directMetadata = client.getMetadata(); Assert.assertNotNull("Direct metadata should not be null", directMetadata); - - Assert.assertEquals("Hook metadata project should match current metadata", + + Assert.assertEquals("Hook metadata project should match current metadata", directMetadata.project.id, capturedMetadata[0].project.id); - Assert.assertEquals("Hook metadata environment should match current metadata", + Assert.assertEquals("Hook metadata environment should match current metadata", directMetadata.environment.id, capturedMetadata[0].environment.id); } @@ -1162,26 +1156,26 @@ public void variable_withMultipleHooks_allReceiveMetadata() throws DevCycleExcep // First hook client.addHook(new EvalHook() { - @Override - public void after(HookContext ctx, Variable variable, VariableMetadata variableMetadata) { - Assert.assertNotNull("First hook should receive metadata", ctx.getMetadata()); - Assert.assertNotNull("First hook metadata should have project", ctx.getMetadata().project); - metadataChecked[0] = true; - } - }); - - // Second hook - client.addHook(new EvalHook() { - @Override - public void after(HookContext ctx, Variable variable, VariableMetadata variableMetadata) { - Assert.assertNotNull("Second hook should receive metadata", ctx.getMetadata()); - Assert.assertNotNull("Second hook metadata should have environment", ctx.getMetadata().environment); - metadataChecked[1] = true; - } + @Override + public void after(HookContext ctx, Variable variable, VariableMetadata variableMetadata) { + Assert.assertNotNull("First hook should receive metadata", ctx.getMetadata()); + Assert.assertNotNull("First hook metadata should have project", ctx.getMetadata().project); + metadataChecked[0] = true; + } + }); + + // Second hook + client.addHook(new EvalHook() { + @Override + public void after(HookContext ctx, Variable variable, VariableMetadata variableMetadata) { + Assert.assertNotNull("Second hook should receive metadata", ctx.getMetadata()); + Assert.assertNotNull("Second hook metadata should have environment", ctx.getMetadata().environment); + metadataChecked[1] = true; + } }); Variable result = client.variable(user, "string-var", "default string"); - + Assert.assertTrue("First hook should have checked metadata", metadataChecked[0]); Assert.assertTrue("Second hook should have checked metadata", metadataChecked[1]); } @@ -1211,7 +1205,7 @@ public void configMetadata_canBeConstructedWithMockData() { // Verify that metadata can be used in HookContext DevCycleUser testUser = DevCycleUser.builder().userId("test-user").build(); HookContext contextWithMetadata = new HookContext<>(testUser, "test-key", "default", metadata); - + Assert.assertNotNull("HookContext should not be null", contextWithMetadata); Assert.assertEquals("Metadata should be accessible from context", metadata, contextWithMetadata.getMetadata()); } From 958c640d15b72e0a7eb96e10260f82cd9d0620a6 Mon Sep 17 00:00:00 2001 From: Jamie Sinn Date: Wed, 18 Feb 2026 11:47:28 -0500 Subject: [PATCH 2/2] Fix review comments --- .../devcycle/sdk/server/common/api/APIUtils.java | 12 ++++++------ .../sdk/server/common/api/IRestOptions.java | 16 +++++++++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/devcycle/sdk/server/common/api/APIUtils.java b/src/main/java/com/devcycle/sdk/server/common/api/APIUtils.java index cdd3c841..465c8a4e 100644 --- a/src/main/java/com/devcycle/sdk/server/common/api/APIUtils.java +++ b/src/main/java/com/devcycle/sdk/server/common/api/APIUtils.java @@ -12,12 +12,12 @@ public static void applyRestOptions(IRestOptions restOptions, OkHttpClient.Build if (restOptions.getProxy() != null) { builder.proxy(restOptions.getProxy()); - if (restOptions.getProxyAuthenticator() != null) { - builder.proxyAuthenticator(restOptions.getProxyAuthenticator()); - } - if (restOptions.getProxySelector() != null) { - builder.proxySelector(restOptions.getProxySelector()); - } + } + if (restOptions.getProxyAuthenticator() != null) { + builder.proxyAuthenticator(restOptions.getProxyAuthenticator()); + } + if (restOptions.getProxySelector() != null) { + builder.proxySelector(restOptions.getProxySelector()); } if (restOptions.getSocketFactory() != null && restOptions.getTrustManager() != null) { diff --git a/src/main/java/com/devcycle/sdk/server/common/api/IRestOptions.java b/src/main/java/com/devcycle/sdk/server/common/api/IRestOptions.java index 2e612719..705ebbbc 100644 --- a/src/main/java/com/devcycle/sdk/server/common/api/IRestOptions.java +++ b/src/main/java/com/devcycle/sdk/server/common/api/IRestOptions.java @@ -37,15 +37,21 @@ public interface IRestOptions { /** * @return a Proxy to use when making requests. Return null if the default Proxy selector can be used */ - Proxy getProxy(); + default Proxy getProxy() { + return null; + } /** - * @return a ProxySelector to use when making requests. Return null if the default Proxy selector can be used. Requires the proxy() method to be implemented as well. + * @return a ProxySelector to use when making requests. Return null if the default Proxy selector can be used. */ - ProxySelector getProxySelector(); + default ProxySelector getProxySelector() { + return null; + } /** - * @return an Authenticator to use when making requests. Return null if the default Authenticator can be used. Requires the proxy() method to be implemented as well. + * @return an Authenticator to use when making requests. Return null if the default Authenticator can be used. */ - Authenticator getProxyAuthenticator(); + default Authenticator getProxyAuthenticator(){ + return null; + } }