From a89756462e006299c6c1e1f7cabefad277577dec Mon Sep 17 00:00:00 2001 From: Jeffrey Parker Date: Mon, 23 Feb 2026 15:33:25 -0500 Subject: [PATCH 1/9] Add configurable maxBackoffMs for rate limit retries Allow callers to configure the maximum backoff threshold for 429 retry logic via a new useMaxBackoffMs() builder method. Setting maxBackoffMs to 0 disables retries entirely. The default (32000ms) preserves existing behavior. --- .../java/com/duosecurity/client/Http.java | 24 ++++- .../client/HttpRateLimitRetryTest.java | 88 ++++++++++++++++++- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/duo-client/src/main/java/com/duosecurity/client/Http.java b/duo-client/src/main/java/com/duosecurity/client/Http.java index fadb3fa..4f9c05a 100644 --- a/duo-client/src/main/java/com/duosecurity/client/Http.java +++ b/duo-client/src/main/java/com/duosecurity/client/Http.java @@ -42,6 +42,7 @@ public class Http { private Headers.Builder headers; private SortedMap params = new TreeMap(); protected int sigVersion = 5; + private long maxBackoffMs = MAX_BACKOFF_MS; private Random random = new Random(); private OkHttpClient httpClient; private SortedMap additionalDuoHeaders = new TreeMap(); @@ -314,7 +315,7 @@ private Response executeRequest(Request request) throws Exception { long backoffMs = INITIAL_BACKOFF_MS; while (true) { Response response = httpClient.newCall(request).execute(); - if (response.code() != RATE_LIMIT_ERROR_CODE || backoffMs > MAX_BACKOFF_MS) { + if (response.code() != RATE_LIMIT_ERROR_CODE || backoffMs > maxBackoffMs) { return response; } @@ -327,6 +328,10 @@ protected void sleep(long ms) throws Exception { Thread.sleep(ms); } + protected void setMaxBackoffMs(long maxBackoffMs) { + this.maxBackoffMs = maxBackoffMs; + } + public void signRequest(String ikey, String skey) throws UnsupportedEncodingException { signRequest(ikey, skey, sigVersion); @@ -529,6 +534,7 @@ protected abstract static class ClientBuilder { private final String uri; private int timeout = DEFAULT_TIMEOUT_SECS; + private long maxBackoffMs = MAX_BACKOFF_MS; private String[] caCerts = null; private SortedMap additionalDuoHeaders = new TreeMap(); private Map headers = new HashMap(); @@ -558,6 +564,21 @@ public ClientBuilder useTimeout(int timeout) { return this; } + /** + * Set the maximum backoff time in milliseconds for rate limit (429) retries. + * When a request receives a 429 response, the client retries with exponential + * backoff until the backoff exceeds this threshold. Setting to 0 disables retries. + * Default is 32000ms (32 seconds). + * + * @param maxBackoffMs the maximum backoff in milliseconds + * @return the Builder + */ + public ClientBuilder useMaxBackoffMs(long maxBackoffMs) { + this.maxBackoffMs = maxBackoffMs; + + return this; + } + /** * Provide custom CA certificates for certificate pinning. * @@ -604,6 +625,7 @@ public ClientBuilder addHeader(String name, String value) { */ public T build() { T duoClient = createClient(method, host, uri, timeout); + duoClient.setMaxBackoffMs(maxBackoffMs); if (caCerts != null) { duoClient.useCustomCertificates(caCerts); } diff --git a/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java b/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java index 5955e2c..03d7fe0 100644 --- a/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java +++ b/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java @@ -26,10 +26,8 @@ public class HttpRateLimitRetryTest { private final int RANDOM_INT = 234; - @Before - public void before() throws Exception { - http = new Http.HttpBuilder("GET", "example.test", "/foo/bar").build(); - http = Mockito.spy(http); + private void setupHttp(Http client) throws Exception { + http = Mockito.spy(client); Field httpClientField = Http.class.getDeclaredField("httpClient"); httpClientField.setAccessible(true); @@ -39,6 +37,12 @@ public void before() throws Exception { Mockito.doNothing().when(http).sleep(Mockito.any(Long.class)); } + @Before + public void before() throws Exception { + Http client = new Http.HttpBuilder("GET", "example.test", "/foo/bar").build(); + setupHttp(client); + } + @Test public void testSingleRateLimitRetry() throws Exception { final List responses = new ArrayList(); @@ -128,4 +132,80 @@ public Call answer(InvocationOnMock invocationOnMock) throws Throwable { assertEquals(16000L + RANDOM_INT, (long) sleepTimes.get(4)); assertEquals(32000L + RANDOM_INT, (long) sleepTimes.get(5)); } + + @Test + public void testMaxBackoffZeroDisablesRetry() throws Exception { + Http customHttp = new Http.HttpBuilder("GET", "example.test", "/foo/bar") + .useMaxBackoffMs(0) + .build(); + setupHttp(customHttp); + + final List responses = new ArrayList(); + + Mockito.when(httpClient.newCall(Mockito.any(Request.class))).thenAnswer(new Answer() { + @Override + public Call answer(InvocationOnMock invocationOnMock) throws Throwable { + Call call = Mockito.mock(Call.class); + + Response resp = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(429) + .request((Request) invocationOnMock.getArguments()[0]) + .message("HTTP 429") + .build(); + responses.add(resp); + Mockito.when(call.execute()).thenReturn(resp); + + return call; + } + }); + + Response actualRes = http.executeHttpRequest(); + assertEquals(1, responses.size()); + assertEquals(429, actualRes.code()); + + // Verify no sleep was called + Mockito.verify(http, Mockito.never()).sleep(Mockito.any(Long.class)); + } + + @Test + public void testMaxBackoffCustomLimit() throws Exception { + Http customHttp = new Http.HttpBuilder("GET", "example.test", "/foo/bar") + .useMaxBackoffMs(4000) + .build(); + setupHttp(customHttp); + + final List responses = new ArrayList(); + + Mockito.when(httpClient.newCall(Mockito.any(Request.class))).thenAnswer(new Answer() { + @Override + public Call answer(InvocationOnMock invocationOnMock) throws Throwable { + Call call = Mockito.mock(Call.class); + + Response resp = new Response.Builder() + .protocol(Protocol.HTTP_2) + .code(429) + .request((Request) invocationOnMock.getArguments()[0]) + .message("HTTP 429") + .build(); + responses.add(resp); + Mockito.when(call.execute()).thenReturn(resp); + + return call; + } + }); + + // With maxBackoff=4000, retries at 1000, 2000, 4000, then 8000 > 4000 exits + // That's 4 total requests (1 initial + 3 retries) + Response actualRes = http.executeHttpRequest(); + assertEquals(4, responses.size()); + assertEquals(429, actualRes.code()); + + ArgumentCaptor sleepCapture = ArgumentCaptor.forClass(Long.class); + Mockito.verify(http, Mockito.times(3)).sleep(sleepCapture.capture()); + List sleepTimes = sleepCapture.getAllValues(); + assertEquals(1000L + RANDOM_INT, (long) sleepTimes.get(0)); + assertEquals(2000L + RANDOM_INT, (long) sleepTimes.get(1)); + assertEquals(4000L + RANDOM_INT, (long) sleepTimes.get(2)); + } } From 8360f33db1bea9e4718ab2c96dd979f1b0e74ca2 Mon Sep 17 00:00:00 2001 From: Jeffrey Parker Date: Wed, 25 Feb 2026 12:10:09 -0500 Subject: [PATCH 2/9] Add input validation and improve docs for maxBackoffMs - Reject negative values in both useMaxBackoffMs() builder method and setMaxBackoffMs() setter with IllegalArgumentException - Clarify Javadoc that maxBackoffMs is the base backoff threshold before jitter (actual sleep includes up to 1000ms random jitter) - Add test for negative value rejection --- .../main/java/com/duosecurity/client/Http.java | 16 ++++++++++++---- .../client/HttpRateLimitRetryTest.java | 7 +++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/duo-client/src/main/java/com/duosecurity/client/Http.java b/duo-client/src/main/java/com/duosecurity/client/Http.java index 4f9c05a..ec13cf3 100644 --- a/duo-client/src/main/java/com/duosecurity/client/Http.java +++ b/duo-client/src/main/java/com/duosecurity/client/Http.java @@ -329,6 +329,9 @@ protected void sleep(long ms) throws Exception { } protected void setMaxBackoffMs(long maxBackoffMs) { + if (maxBackoffMs < 0) { + throw new IllegalArgumentException("maxBackoffMs must be >= 0"); + } this.maxBackoffMs = maxBackoffMs; } @@ -565,15 +568,20 @@ public ClientBuilder useTimeout(int timeout) { } /** - * Set the maximum backoff time in milliseconds for rate limit (429) retries. + * Set the maximum base backoff time in milliseconds for rate limit (429) retries. * When a request receives a 429 response, the client retries with exponential - * backoff until the backoff exceeds this threshold. Setting to 0 disables retries. - * Default is 32000ms (32 seconds). + * backoff until the base backoff exceeds this threshold. Note that actual sleep + * time includes up to 1000ms of random jitter on top of the base backoff. + * Setting to 0 disables retries. Default is 32000ms (32 seconds). * - * @param maxBackoffMs the maximum backoff in milliseconds + * @param maxBackoffMs the maximum base backoff in milliseconds (must be >= 0) * @return the Builder + * @throws IllegalArgumentException if maxBackoffMs is negative */ public ClientBuilder useMaxBackoffMs(long maxBackoffMs) { + if (maxBackoffMs < 0) { + throw new IllegalArgumentException("maxBackoffMs must be >= 0"); + } this.maxBackoffMs = maxBackoffMs; return this; diff --git a/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java b/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java index 03d7fe0..baf41fb 100644 --- a/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java +++ b/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java @@ -208,4 +208,11 @@ public Call answer(InvocationOnMock invocationOnMock) throws Throwable { assertEquals(2000L + RANDOM_INT, (long) sleepTimes.get(1)); assertEquals(4000L + RANDOM_INT, (long) sleepTimes.get(2)); } + + @Test(expected = IllegalArgumentException.class) + public void testMaxBackoffNegativeThrows() { + new Http.HttpBuilder("GET", "example.test", "/foo/bar") + .useMaxBackoffMs(-1) + .build(); + } } From fcf5cd850647470449a332bd08bdbdf5ad6b1456 Mon Sep 17 00:00:00 2001 From: Jeffrey Parker Date: Tue, 10 Mar 2026 16:42:42 -0400 Subject: [PATCH 3/9] Add unit test confirming default maxBackoffMs of 32000 is used when not specified --- .../duosecurity/client/HttpRateLimitRetryTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java b/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java index baf41fb..ad3c9f5 100644 --- a/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java +++ b/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java @@ -209,6 +209,17 @@ public Call answer(InvocationOnMock invocationOnMock) throws Throwable { assertEquals(4000L + RANDOM_INT, (long) sleepTimes.get(2)); } + @Test + public void testDefaultMaxBackoffIsUsedWhenNotSpecified() throws Exception { + Http defaultHttp = new Http.HttpBuilder("GET", "example.test", "/foo/bar").build(); + + Field maxBackoffField = Http.class.getDeclaredField("maxBackoffMs"); + maxBackoffField.setAccessible(true); + long actualMaxBackoff = (long) maxBackoffField.get(defaultHttp); + + assertEquals(Http.MAX_BACKOFF_MS, actualMaxBackoff); + } + @Test(expected = IllegalArgumentException.class) public void testMaxBackoffNegativeThrows() { new Http.HttpBuilder("GET", "example.test", "/foo/bar") From 341b3676646806b527e74a75c25ad06f04709fc8 Mon Sep 17 00:00:00 2001 From: Jeffrey Parker Date: Tue, 10 Mar 2026 16:42:52 -0400 Subject: [PATCH 4/9] Add integration tests for rate limit retry and backoff using MockWebServer Tests use a real HTTPS server (OkHttp MockWebServer + okhttp-tls) to verify end-to-end retry behavior: single retry with success, exhausting the default 32s max backoff, a custom max backoff limit, and zero backoff disabling retries. --- duo-client/pom.xml | 12 ++ .../HttpRateLimitRetryIntegrationTest.java | 138 ++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryIntegrationTest.java diff --git a/duo-client/pom.xml b/duo-client/pom.xml index c1a2c29..7caaeca 100644 --- a/duo-client/pom.xml +++ b/duo-client/pom.xml @@ -65,6 +65,18 @@ 3.12.4 test + + com.squareup.okhttp3 + mockwebserver + 4.12.0 + test + + + com.squareup.okhttp3 + okhttp-tls + 4.12.0 + test + diff --git a/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryIntegrationTest.java b/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryIntegrationTest.java new file mode 100644 index 0000000..a4e689b --- /dev/null +++ b/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryIntegrationTest.java @@ -0,0 +1,138 @@ +package com.duosecurity.client; + +import okhttp3.OkHttpClient; +import okhttp3.Response; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.tls.HandshakeCertificates; +import okhttp3.tls.HeldCertificate; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.lang.reflect.Field; + +import static org.junit.Assert.assertEquals; + +public class HttpRateLimitRetryIntegrationTest { + + private MockWebServer server; + private HandshakeCertificates clientCerts; + + @Before + public void setUp() throws Exception { + HeldCertificate serverCert = new HeldCertificate.Builder() + .addSubjectAlternativeName("localhost") + .build(); + + HandshakeCertificates serverCerts = new HandshakeCertificates.Builder() + .heldCertificate(serverCert) + .build(); + + clientCerts = new HandshakeCertificates.Builder() + .addTrustedCertificate(serverCert.certificate()) + .build(); + + server = new MockWebServer(); + server.useHttps(serverCerts.sslSocketFactory(), false); + server.start(); + } + + @After + public void tearDown() throws Exception { + server.shutdown(); + } + + /** + * Builds an Http spy pointing at the MockWebServer, with sleep() stubbed out to avoid real + * delays and the OkHttpClient replaced with one that trusts the test certificate. + * + *

The builder must be constructed with host "localhost" (no port) so that CertificatePinner + * accepts the pattern. This method then sets the real host (with port) and replaces the + * OkHttpClient via reflection before the spy is used. + */ + private Http buildSpyHttp(Http.ClientBuilder builder) throws Exception { + Http spy = Mockito.spy(builder.build()); + Mockito.doNothing().when(spy).sleep(Mockito.any(Long.class)); + + // Point the host at the MockWebServer port (CertificatePinner rejects host:port patterns, + // so the builder uses "localhost" and we fix it here after construction). + Field hostField = Http.class.getDeclaredField("host"); + hostField.setAccessible(true); + hostField.set(spy, "localhost:" + server.getPort()); + + // Replace the OkHttpClient with one configured to trust the test certificate + OkHttpClient testClient = new OkHttpClient.Builder() + .sslSocketFactory(clientCerts.sslSocketFactory(), clientCerts.trustManager()) + .build(); + + Field httpClientField = Http.class.getDeclaredField("httpClient"); + httpClientField.setAccessible(true); + httpClientField.set(spy, testClient); + + return spy; + } + + private Http.HttpBuilder defaultBuilder() { + // Use "localhost" without a port — CertificatePinner rejects host:port patterns. + // buildSpyHttp sets the real host (with port) via reflection after construction. + return new Http.HttpBuilder("GET", "localhost", "/foo/bar"); + } + + @Test + public void testSingleRateLimitRetry() throws Exception { + server.enqueue(new MockResponse().setResponseCode(429)); + server.enqueue(new MockResponse().setResponseCode(200)); + + Http http = buildSpyHttp(defaultBuilder()); + Response response = http.executeHttpRequest(); + + assertEquals(200, response.code()); + assertEquals(2, server.getRequestCount()); + Mockito.verify(http, Mockito.times(1)).sleep(Mockito.any(Long.class)); + } + + @Test + public void testRateLimitExhaustsDefaultMaxBackoff() throws Exception { + // Enqueue more responses than will ever be consumed + for (int i = 0; i < 10; i++) { + server.enqueue(new MockResponse().setResponseCode(429)); + } + + Http http = buildSpyHttp(defaultBuilder()); + Response response = http.executeHttpRequest(); + + assertEquals(429, response.code()); + // Default max backoff (32s): sleeps at 1s, 2s, 4s, 8s, 16s, 32s = 6 sleeps, 7 total requests + assertEquals(7, server.getRequestCount()); + Mockito.verify(http, Mockito.times(6)).sleep(Mockito.any(Long.class)); + } + + @Test + public void testCustomMaxBackoffLimitsRetries() throws Exception { + for (int i = 0; i < 10; i++) { + server.enqueue(new MockResponse().setResponseCode(429)); + } + + Http http = buildSpyHttp(defaultBuilder().useMaxBackoffMs(4000)); + Response response = http.executeHttpRequest(); + + assertEquals(429, response.code()); + // maxBackoff=4000: sleeps at 1s, 2s, 4s = 3 sleeps, 4 total requests (next would be 8s > 4s) + assertEquals(4, server.getRequestCount()); + Mockito.verify(http, Mockito.times(3)).sleep(Mockito.any(Long.class)); + } + + @Test + public void testMaxBackoffZeroDisablesRetry() throws Exception { + server.enqueue(new MockResponse().setResponseCode(429)); + + Http http = buildSpyHttp(defaultBuilder().useMaxBackoffMs(0)); + Response response = http.executeHttpRequest(); + + assertEquals(429, response.code()); + assertEquals(1, server.getRequestCount()); + Mockito.verify(http, Mockito.never()).sleep(Mockito.any(Long.class)); + } +} From cb216dddf2b2cc50cb8af9e91e71ff84a351f185 Mon Sep 17 00:00:00 2001 From: Jeffrey Parker Date: Tue, 10 Mar 2026 17:10:19 -0400 Subject: [PATCH 5/9] Close intermediate 429 responses before retry to prevent resource leaks --- duo-client/src/main/java/com/duosecurity/client/Http.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/duo-client/src/main/java/com/duosecurity/client/Http.java b/duo-client/src/main/java/com/duosecurity/client/Http.java index ec13cf3..8415ca3 100644 --- a/duo-client/src/main/java/com/duosecurity/client/Http.java +++ b/duo-client/src/main/java/com/duosecurity/client/Http.java @@ -319,6 +319,10 @@ private Response executeRequest(Request request) throws Exception { return response; } + // Close the 429 response to release the connection back to the pool before retrying + if (response.body() != null) { + response.close(); + } sleep(backoffMs + nextRandomInt(1000)); backoffMs *= BACKOFF_FACTOR; } From 03fa38c42f9342445573aeb9e04ab9ae1e029716 Mon Sep 17 00:00:00 2001 From: Jeffrey Parker Date: Tue, 10 Mar 2026 17:10:29 -0400 Subject: [PATCH 6/9] Improve useMaxBackoffMs Javadoc accuracy and document chaining limitation Clarify that values below INITIAL_BACKOFF_MS (1000ms) also effectively disable retries, and document the ClientBuilder visibility limitation that prevents fluent method chaining from outside the package. --- duo-client/src/main/java/com/duosecurity/client/Http.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/duo-client/src/main/java/com/duosecurity/client/Http.java b/duo-client/src/main/java/com/duosecurity/client/Http.java index 8415ca3..d897478 100644 --- a/duo-client/src/main/java/com/duosecurity/client/Http.java +++ b/duo-client/src/main/java/com/duosecurity/client/Http.java @@ -576,7 +576,13 @@ public ClientBuilder useTimeout(int timeout) { * When a request receives a 429 response, the client retries with exponential * backoff until the base backoff exceeds this threshold. Note that actual sleep * time includes up to 1000ms of random jitter on top of the base backoff. - * Setting to 0 disables retries. Default is 32000ms (32 seconds). + * Setting to 0 disables retries (as does any value below the initial + * backoff of 1000ms). Default is 32000ms (32 seconds). + * + *

Note: When using method chaining from outside this package (e.g. with + * {@code AuthBuilder} or {@code AdminBuilder}), assign the builder to a variable + * and call methods separately, then call {@code build()}. This is a known + * limitation of all {@code ClientBuilder} methods. * * @param maxBackoffMs the maximum base backoff in milliseconds (must be >= 0) * @return the Builder From 63376d62edc08946055214f21345aa1a8ff2777b Mon Sep 17 00:00:00 2001 From: Jeffrey Parker Date: Tue, 10 Mar 2026 17:10:34 -0400 Subject: [PATCH 7/9] Move integration tests to maven-failsafe-plugin Rename HttpRateLimitRetryIntegrationTest to *IT (failsafe naming convention) and add maven-failsafe-plugin so socket-binding tests run in the integration-test phase instead of the default test phase. --- duo-client/pom.xml | 13 +++++++++++++ ...st.java => HttpRateLimitRetryIntegrationIT.java} | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) rename duo-client/src/test/java/com/duosecurity/client/{HttpRateLimitRetryIntegrationTest.java => HttpRateLimitRetryIntegrationIT.java} (99%) diff --git a/duo-client/pom.xml b/duo-client/pom.xml index 7caaeca..7957502 100644 --- a/duo-client/pom.xml +++ b/duo-client/pom.xml @@ -135,6 +135,19 @@ methods + + org.apache.maven.plugins + maven-failsafe-plugin + 3.2.5 + + + + integration-test + verify + + + + org.cyclonedx cyclonedx-maven-plugin diff --git a/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryIntegrationTest.java b/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryIntegrationIT.java similarity index 99% rename from duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryIntegrationTest.java rename to duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryIntegrationIT.java index a4e689b..a127b71 100644 --- a/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryIntegrationTest.java +++ b/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryIntegrationIT.java @@ -15,7 +15,7 @@ import static org.junit.Assert.assertEquals; -public class HttpRateLimitRetryIntegrationTest { +public class HttpRateLimitRetryIntegrationIT { private MockWebServer server; private HandshakeCertificates clientCerts; From 50405c564707abf2f69bf5876653e1217e880c52 Mon Sep 17 00:00:00 2001 From: Jeffrey Parker Date: Tue, 10 Mar 2026 17:10:39 -0400 Subject: [PATCH 8/9] Remove redundant reflection-based default maxBackoffMs test The testRepeatRetryAfterRateLimit test already covers default backoff behavior (7 requests = 32s max) through observable retry count. --- .../duosecurity/client/HttpRateLimitRetryTest.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java b/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java index ad3c9f5..baf41fb 100644 --- a/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java +++ b/duo-client/src/test/java/com/duosecurity/client/HttpRateLimitRetryTest.java @@ -209,17 +209,6 @@ public Call answer(InvocationOnMock invocationOnMock) throws Throwable { assertEquals(4000L + RANDOM_INT, (long) sleepTimes.get(2)); } - @Test - public void testDefaultMaxBackoffIsUsedWhenNotSpecified() throws Exception { - Http defaultHttp = new Http.HttpBuilder("GET", "example.test", "/foo/bar").build(); - - Field maxBackoffField = Http.class.getDeclaredField("maxBackoffMs"); - maxBackoffField.setAccessible(true); - long actualMaxBackoff = (long) maxBackoffField.get(defaultHttp); - - assertEquals(Http.MAX_BACKOFF_MS, actualMaxBackoff); - } - @Test(expected = IllegalArgumentException.class) public void testMaxBackoffNegativeThrows() { new Http.HttpBuilder("GET", "example.test", "/foo/bar") From a481565fb157376686b3227fbb73cbcc30d78c12 Mon Sep 17 00:00:00 2001 From: Jeffrey Parker Date: Tue, 10 Mar 2026 17:28:32 -0400 Subject: [PATCH 9/9] Run integration tests in CI by using mvn verify --- .github/workflows/java-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/java-ci.yml b/.github/workflows/java-ci.yml index 50d7741..5e2b535 100644 --- a/.github/workflows/java-ci.yml +++ b/.github/workflows/java-ci.yml @@ -33,8 +33,8 @@ jobs: --file duo-client/pom.xml - name: Test with Maven run: > - mvn test - --batch-mode + mvn verify + --batch-mode -file duo-client/pom.xml - name: Lint with checkstyle run: mvn checkstyle:check