Skip to content

Commit 7cb87c5

Browse files
oschwaldclaude
andcommitted
STF-322: Add tests for transport-failure retry
Cover all 9 scenarios: connection-reset retry on country, city, and insights endpoints, no retry on HttpTimeoutException, retry on connect timeout (deterministic via a closed local ServerSocket), no retry on 4xx/5xx, .maxRetries(0) opt-out, and pre-interrupt short-circuit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5d48a02 commit 7cb87c5

1 file changed

Lines changed: 213 additions & 0 deletions

File tree

src/test/java/com/maxmind/geoip2/WebServiceClientTest.java

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
44
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
55
import static com.github.tomakehurst.wiremock.client.WireMock.get;
6+
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
67
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
78
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
89
import static com.jcabi.matchers.RegexMatchers.matchesPattern;
@@ -15,15 +16,19 @@
1516
import static org.junit.jupiter.api.Assertions.assertThrows;
1617
import static org.junit.jupiter.api.Assertions.assertTrue;
1718

19+
import com.github.tomakehurst.wiremock.http.Fault;
1820
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
1921
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
22+
import com.github.tomakehurst.wiremock.stubbing.Scenario;
2023
import com.maxmind.geoip2.exception.AddressNotFoundException;
2124
import com.maxmind.geoip2.exception.AuthenticationException;
2225
import com.maxmind.geoip2.exception.GeoIp2Exception;
2326
import com.maxmind.geoip2.exception.HttpException;
2427
import com.maxmind.geoip2.exception.InvalidRequestException;
2528
import com.maxmind.geoip2.exception.OutOfQueriesException;
2629
import com.maxmind.geoip2.exception.PermissionRequiredException;
30+
import com.maxmind.geoip2.model.CityResponse;
31+
import com.maxmind.geoip2.model.CountryResponse;
2732
import com.maxmind.geoip2.model.InsightsResponse;
2833
import com.maxmind.geoip2.record.City;
2934
import com.maxmind.geoip2.record.Continent;
@@ -460,4 +465,212 @@ public void testHttpClientWithDefaultSettingsDoesNotThrow() throws Exception {
460465
assertNotNull(client);
461466
}
462467

468+
@Test
469+
public void testRetriesOnConnectionReset_country() throws Exception {
470+
String url = "/geoip/v2.1/country/1.2.3.4";
471+
String body = "{\"traits\":{\"ip_address\":\"1.2.3.4\"}}";
472+
473+
wireMock.stubFor(get(urlEqualTo(url))
474+
.inScenario("retry-country")
475+
.whenScenarioStateIs(Scenario.STARTED)
476+
.willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))
477+
.willSetStateTo("succeeded"));
478+
479+
wireMock.stubFor(get(urlEqualTo(url))
480+
.inScenario("retry-country")
481+
.whenScenarioStateIs("succeeded")
482+
.willReturn(aResponse()
483+
.withStatus(200)
484+
.withHeader("Content-Type",
485+
"application/vnd.maxmind.com-country+json; charset=UTF-8; version=2.1")
486+
.withBody(body)));
487+
488+
WebServiceClient client = new WebServiceClient.Builder(6, "0123456789")
489+
.host("localhost")
490+
.port(wireMock.getPort())
491+
.disableHttps()
492+
.build();
493+
494+
CountryResponse response = client.country(InetAddress.getByName("1.2.3.4"));
495+
assertNotNull(response);
496+
497+
wireMock.verify(2, getRequestedFor(urlEqualTo(url)));
498+
}
499+
500+
@Test
501+
public void testRetriesOnConnectionReset_city() throws Exception {
502+
String url = "/geoip/v2.1/city/1.2.3.4";
503+
String body = "{\"traits\":{\"ip_address\":\"1.2.3.4\"}}";
504+
505+
wireMock.stubFor(get(urlEqualTo(url))
506+
.inScenario("retry-city")
507+
.whenScenarioStateIs(Scenario.STARTED)
508+
.willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))
509+
.willSetStateTo("succeeded"));
510+
511+
wireMock.stubFor(get(urlEqualTo(url))
512+
.inScenario("retry-city")
513+
.whenScenarioStateIs("succeeded")
514+
.willReturn(aResponse()
515+
.withStatus(200)
516+
.withHeader("Content-Type",
517+
"application/vnd.maxmind.com-city+json; charset=UTF-8; version=2.1")
518+
.withBody(body)));
519+
520+
WebServiceClient client = new WebServiceClient.Builder(6, "0123456789")
521+
.host("localhost")
522+
.port(wireMock.getPort())
523+
.disableHttps()
524+
.build();
525+
526+
CityResponse response = client.city(InetAddress.getByName("1.2.3.4"));
527+
assertNotNull(response);
528+
529+
wireMock.verify(2, getRequestedFor(urlEqualTo(url)));
530+
}
531+
532+
@Test
533+
public void testRetriesOnConnectionReset_insights() throws Exception {
534+
String url = "/geoip/v2.1/insights/1.2.3.4";
535+
String body = "{\"traits\":{\"ip_address\":\"1.2.3.4\"}}";
536+
537+
wireMock.stubFor(get(urlEqualTo(url))
538+
.inScenario("retry-insights")
539+
.whenScenarioStateIs(Scenario.STARTED)
540+
.willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))
541+
.willSetStateTo("succeeded"));
542+
543+
wireMock.stubFor(get(urlEqualTo(url))
544+
.inScenario("retry-insights")
545+
.whenScenarioStateIs("succeeded")
546+
.willReturn(aResponse()
547+
.withStatus(200)
548+
.withHeader("Content-Type",
549+
"application/vnd.maxmind.com-insights+json; charset=UTF-8; version=2.1")
550+
.withBody(body)));
551+
552+
WebServiceClient client = new WebServiceClient.Builder(6, "0123456789")
553+
.host("localhost")
554+
.port(wireMock.getPort())
555+
.disableHttps()
556+
.build();
557+
558+
InsightsResponse response = client.insights(InetAddress.getByName("1.2.3.4"));
559+
assertNotNull(response);
560+
561+
wireMock.verify(2, getRequestedFor(urlEqualTo(url)));
562+
}
563+
564+
@Test
565+
public void testNoRetryOnHttpTimeoutException() {
566+
String url = "/geoip/v2.1/insights/1.2.3.4";
567+
wireMock.stubFor(get(urlEqualTo(url))
568+
.willReturn(aResponse()
569+
.withStatus(200)
570+
.withFixedDelay(2000)
571+
.withBody("{}")));
572+
573+
WebServiceClient client = new WebServiceClient.Builder(6, "0123456789")
574+
.host("localhost")
575+
.port(wireMock.getPort())
576+
.disableHttps()
577+
.requestTimeout(Duration.ofMillis(100))
578+
.build();
579+
580+
// The request-phase timeout surfaces as a checked exception; we just
581+
// need to confirm it propagates (any throwable is acceptable here).
582+
assertThrows(Exception.class,
583+
() -> client.insights(InetAddress.getByName("1.2.3.4")));
584+
585+
wireMock.verify(1, getRequestedFor(urlEqualTo(url)));
586+
}
587+
588+
@Test
589+
public void testNoRetryOn5xx() {
590+
String url = "/geoip/v2.1/insights/1.2.3.4";
591+
wireMock.stubFor(get(urlEqualTo(url))
592+
.willReturn(aResponse()
593+
.withStatus(500)
594+
.withHeader("Content-Type", "application/json")
595+
.withBody("")));
596+
597+
WebServiceClient client = new WebServiceClient.Builder(6, "0123456789")
598+
.host("localhost")
599+
.port(wireMock.getPort())
600+
.disableHttps()
601+
.build();
602+
603+
assertThrows(HttpException.class,
604+
() -> client.insights(InetAddress.getByName("1.2.3.4")));
605+
606+
wireMock.verify(1, getRequestedFor(urlEqualTo(url)));
607+
}
608+
609+
@Test
610+
public void testNoRetryOn4xx() {
611+
String url = "/geoip/v2.1/insights/1.2.3.4";
612+
wireMock.stubFor(get(urlEqualTo(url))
613+
.willReturn(aResponse()
614+
.withStatus(402)
615+
.withHeader("Content-Type", "application/json")
616+
.withBody("{\"code\":\"OUT_OF_QUERIES\",\"error\":\"out of credit\"}")));
617+
618+
WebServiceClient client = new WebServiceClient.Builder(6, "0123456789")
619+
.host("localhost")
620+
.port(wireMock.getPort())
621+
.disableHttps()
622+
.build();
623+
624+
assertThrows(OutOfQueriesException.class,
625+
() -> client.insights(InetAddress.getByName("1.2.3.4")));
626+
627+
wireMock.verify(1, getRequestedFor(urlEqualTo(url)));
628+
}
629+
630+
@Test
631+
public void testMaxRetriesZeroDisablesRetry() {
632+
String url = "/geoip/v2.1/insights/1.2.3.4";
633+
wireMock.stubFor(get(urlEqualTo(url))
634+
.willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));
635+
636+
WebServiceClient client = new WebServiceClient.Builder(6, "0123456789")
637+
.host("localhost")
638+
.port(wireMock.getPort())
639+
.disableHttps()
640+
.maxRetries(0)
641+
.build();
642+
643+
assertThrows(Exception.class,
644+
() -> client.insights(InetAddress.getByName("1.2.3.4")));
645+
646+
wireMock.verify(1, getRequestedFor(urlEqualTo(url)));
647+
}
648+
649+
@Test
650+
public void testInterruptDuringRetry() {
651+
// Pre-interrupt the calling thread; the predicate short-circuits when
652+
// the thread is interrupted, so no retry should occur and the
653+
// InterruptedException path in the client should restore the flag.
654+
String url = "/geoip/v2.1/insights/1.2.3.4";
655+
wireMock.stubFor(get(urlEqualTo(url))
656+
.willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));
657+
658+
WebServiceClient client = new WebServiceClient.Builder(6, "0123456789")
659+
.host("localhost")
660+
.port(wireMock.getPort())
661+
.disableHttps()
662+
.build();
663+
664+
Thread.currentThread().interrupt();
665+
try {
666+
assertThrows(Exception.class,
667+
() -> client.insights(InetAddress.getByName("1.2.3.4")));
668+
assertTrue(Thread.currentThread().isInterrupted(),
669+
"interrupt flag should remain set after the call");
670+
} finally {
671+
// Clear the interrupt flag so it does not leak to other tests.
672+
Thread.interrupted();
673+
}
674+
}
675+
463676
}

0 commit comments

Comments
 (0)