4343import com .maxmind .minfraud .response .InsightsResponse ;
4444import com .maxmind .minfraud .response .IpRiskReason ;
4545import com .maxmind .minfraud .response .ScoreResponse ;
46+ import java .io .IOException ;
4647import java .net .InetAddress ;
4748import java .net .ProxySelector ;
4849import java .net .http .HttpClient ;
50+ import java .net .http .HttpTimeoutException ;
4951import java .time .Duration ;
5052import java .util .List ;
5153import org .junit .jupiter .api .Test ;
@@ -500,9 +502,7 @@ public void testNoRetryOnHttpTimeoutException() {
500502 .requestTimeout (Duration .ofMillis (100 ))
501503 .build ();
502504
503- // The request-phase timeout surfaces as a checked exception; we just
504- // need to confirm it propagates (any throwable is acceptable here).
505- assertThrows (Exception .class , () -> client .insights (fullTransaction ()));
505+ assertThrows (HttpTimeoutException .class , () -> client .insights (fullTransaction ()));
506506
507507 wireMock .verify (1 , postRequestedFor (urlEqualTo (url )));
508508 }
@@ -561,16 +561,44 @@ public void testMaxRetriesZeroDisablesRetry() {
561561 .maxRetries (0 )
562562 .build ();
563563
564- assertThrows (Exception .class , () -> client .insights (fullTransaction ()));
564+ assertThrows (IOException .class , () -> client .insights (fullTransaction ()));
565565
566566 wireMock .verify (1 , postRequestedFor (urlEqualTo (url )));
567567 }
568568
569+ @ Test
570+ public void testRetriesExhausted () {
571+ var url = "/minfraud/v2.0/insights" ;
572+ wireMock .stubFor (post (urlEqualTo (url ))
573+ .willReturn (aResponse ().withFault (Fault .CONNECTION_RESET_BY_PEER )));
574+
575+ var client = new WebServiceClient .Builder (6 , "0123456789" )
576+ .host ("localhost" )
577+ .port (wireMock .getPort ())
578+ .disableHttps ()
579+ .maxRetries (2 )
580+ .build ();
581+
582+ assertThrows (IOException .class , () -> client .insights (fullTransaction ()));
583+
584+ // 1 initial attempt + 2 retries.
585+ wireMock .verify (3 , postRequestedFor (urlEqualTo (url )));
586+ }
587+
588+ @ Test
589+ public void testNegativeMaxRetriesThrows () {
590+ var builder = new WebServiceClient .Builder (6 , "0123456789" );
591+ assertThrows (IllegalArgumentException .class , () -> builder .maxRetries (-1 ));
592+ }
593+
569594 @ Test
570595 public void testInterruptDuringRetry () {
571- // Pre-interrupt the calling thread; the predicate short-circuits when
572- // the thread is interrupted, so no retry should occur and the
573- // InterruptedException path in the client should restore the flag.
596+ // Pre-interrupting the calling thread aborts the call before any wire
597+ // request is dispatched: HttpClient.send checks the interrupt status
598+ // and throws InterruptedException, which is caught and rewrapped as
599+ // MinFraudException with the interrupt flag restored. The wire-count
600+ // assertion (zero) guards against a regression where pre-interrupt
601+ // would silently let the request proceed.
574602 var url = "/minfraud/v2.0/insights" ;
575603 wireMock .stubFor (post (urlEqualTo (url ))
576604 .willReturn (aResponse ().withFault (Fault .CONNECTION_RESET_BY_PEER )));
@@ -583,12 +611,14 @@ public void testInterruptDuringRetry() {
583611
584612 Thread .currentThread ().interrupt ();
585613 try {
586- assertThrows (Exception .class , () -> client .insights (fullTransaction ()));
614+ assertThrows (MinFraudException .class , () -> client .insights (fullTransaction ()));
587615 assertTrue (Thread .currentThread ().isInterrupted (),
588616 "interrupt flag should remain set after the call" );
589617 } finally {
590- // Clear the interrupt flag so it does not leak to other tests.
618+ // Clear the interrupt flag so it does not leak to other tests
619+ // (and so wireMock.verify below isn't affected by it).
591620 Thread .interrupted ();
592621 }
622+ wireMock .verify (0 , postRequestedFor (urlEqualTo (url )));
593623 }
594624}
0 commit comments