diff options
Diffstat (limited to 'src/test/java/is/cep/cepis/RetryTest.java')
| -rw-r--r-- | src/test/java/is/cep/cepis/RetryTest.java | 105 |
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"); + } +} |