summaryrefslogtreecommitdiffstats
path: root/src/test/java/is/cep/cepis/RetryTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/java/is/cep/cepis/RetryTest.java')
-rw-r--r--src/test/java/is/cep/cepis/RetryTest.java105
1 files changed, 105 insertions, 0 deletions
diff --git a/src/test/java/is/cep/cepis/RetryTest.java b/src/test/java/is/cep/cepis/RetryTest.java
new file mode 100644
index 0000000..702a1ff
--- /dev/null
+++ b/src/test/java/is/cep/cepis/RetryTest.java
@@ -0,0 +1,105 @@
+package is.cep.cepis;
+
+import static is.cep.cepis.Assertions.assertEquals;
+import static is.cep.cepis.Assertions.assertThrows;
+import static is.cep.cepis.Assertions.assertTrue;
+
+import is.cep.cepis.FakeServer.Reply;
+import java.time.Duration;
+
+/** The retry policy: what is retried, how long it waits, and what it never retries. */
+public final class RetryTest extends HttpTest {
+
+ /** A transient 500 is retried and the second attempt is used. */
+ public void testRetriesOn500ThenSucceeds() {
+ server.respond("/01001000",
+ Reply.of(500, "{\"erro\":\"interno\"}"),
+ Reply.of(200, SE_ADDRESS));
+ Address address = client.lookup("01001000");
+ assertEquals("São Paulo", address.localidade(), "the retry succeeded");
+ assertEquals(2, server.count("/01001000"), "two requests");
+ }
+
+ /** Three attempts is the documented default. */
+ public void testExhaustsAttemptsThenThrows() {
+ server.respond("/01001000", Reply.of(503, "{\"erro\":\"interno\"}"));
+ ServerErrorException failure = assertThrows(ServerErrorException.class,
+ () -> client.lookup("01001000"), "the API is down");
+ assertEquals(503, failure.status(), "the status is exposed");
+ assertEquals(3, server.count("/01001000"), "three attempts");
+ }
+
+ /** maxAttempts of one disables retrying. */
+ public void testMaxAttemptsOneDisablesRetry() {
+ Client single = Client.builder()
+ .baseUrl(server.url())
+ .maxAttempts(1)
+ .timeout(Duration.ofSeconds(5))
+ .build();
+ server.respond("/01001000", Reply.of(500, "{}"));
+ assertThrows(ServerErrorException.class, () -> single.lookup("01001000"), "500");
+ assertEquals(1, server.count("/01001000"), "exactly one attempt");
+ }
+
+ /** A 429 is a rate limit, and Retry-After reaches the caller. */
+ public void test429BecomesRateLimitedWithRetryAfter() {
+ server.respond("/01001000", Reply.of(429, "{}", "Retry-After", "7"));
+ RateLimitedException failure = assertThrows(RateLimitedException.class,
+ () -> client.lookup("01001000"), "rate limited");
+ assertEquals(Duration.ofSeconds(7), failure.retryAfter().orElse(null), "retryAfter");
+ }
+
+ /** A 429 with no Retry-After leaves the caller nothing to reschedule with. */
+ public void test429WithoutRetryAfter() {
+ server.respond("/01001000", Reply.of(429, "{}"));
+ RateLimitedException failure = assertThrows(RateLimitedException.class,
+ () -> client.lookup("01001000"), "rate limited");
+ assertTrue(failure.retryAfter().isEmpty(), "the server did not say");
+ }
+
+ /** An absurd Retry-After from a bad proxy must not park the thread. */
+ public void testRetryAfterHonoursTheCap() {
+ server.respond("/01001000", Reply.of(429, "{}", "Retry-After", "3600"));
+ long started = System.nanoTime();
+ assertThrows(RateLimitedException.class, () -> client.lookup("01001000"), "429");
+ long elapsed = (System.nanoTime() - started) / 1_000_000L;
+ assertTrue(elapsed < 5_000, "capped at backoffMax, took " + elapsed + "ms");
+ }
+
+ /** Retry-After also arrives as an HTTP date. */
+ public void testRetryAfterAcceptsAnHttpDate() {
+ Duration parsed = Client.parseRetryAfter("Wed, 21 Oct 2026 07:28:00 GMT");
+ assertTrue(parsed != null, "an HTTP date is understood");
+ assertTrue(parsed.toSeconds() >= 0, "never negative");
+ assertEquals(null, Client.parseRetryAfter("not a date at all"), "nonsense is ignored");
+ assertEquals(Duration.ofSeconds(120), Client.parseRetryAfter(" 120 "), "seconds");
+ }
+
+ /** Nothing listens on port 1, so the connection is refused outright. */
+ public void testNetworkErrorWhenNothingListens() {
+ Client unreachable = Client.builder()
+ .baseUrl("http://127.0.0.1:1")
+ .maxAttempts(2)
+ .backoffBase(Duration.ofMillis(1))
+ .timeout(Duration.ofSeconds(1))
+ .build();
+ assertThrows(NetworkException.class, () -> unreachable.lookup("01001000"), "refused");
+ }
+
+ /** The deadline cuts the run short of the fifty configured attempts. */
+ public void testTotalTimeoutBoundsTheWholeCall() {
+ Client bounded = Client.builder()
+ .baseUrl(server.url())
+ .maxAttempts(50)
+ .backoffBase(Duration.ofMillis(50))
+ .backoffMax(Duration.ofMillis(50))
+ .totalTimeout(Duration.ofMillis(300))
+ .build();
+ server.respond("/01001000", Reply.of(500, "{}"));
+ long started = System.nanoTime();
+ assertThrows(ServerErrorException.class, () -> bounded.lookup("01001000"), "500");
+ long elapsed = (System.nanoTime() - started) / 1_000_000L;
+ assertTrue(elapsed < 3_000, "the deadline held, took " + elapsed + "ms");
+ assertTrue(server.count("/01001000") < 50, "it stopped short of 50 attempts");
+ }
+}