@@ -25,7 +25,7 @@ class ClientServiceTest {
2525 static void startServer () throws IOException {
2626 server = HttpServer .create (new InetSocketAddress (0 ), 0 );
2727 port = server .getAddress ().getPort ();
28- server .setExecutor (Executors .newFixedThreadPool (2 ));
28+ server .setExecutor (Executors .newFixedThreadPool (4 ));
2929
3030 // GET endpoint
3131 server .createContext ("/get" , exchange -> {
@@ -77,6 +77,50 @@ static void startServer() throws IOException {
7777 }
7878 });
7979
80+ // Redirect endpoints
81+ server .createContext ("/redirect/301" , exchange -> {
82+ exchange .getResponseHeaders ().set ("Location" , "/get" );
83+ exchange .sendResponseHeaders (301 , -1 );
84+ exchange .getResponseBody ().close ();
85+ });
86+
87+ server .createContext ("/redirect/302" , exchange -> {
88+ exchange .getResponseHeaders ().set ("Location" , "/get" );
89+ exchange .sendResponseHeaders (302 , -1 );
90+ exchange .getResponseBody ().close ();
91+ });
92+
93+ server .createContext ("/redirect/303" , exchange -> {
94+ exchange .getResponseHeaders ().set ("Location" , "/get" );
95+ exchange .sendResponseHeaders (303 , -1 );
96+ exchange .getResponseBody ().close ();
97+ });
98+
99+ server .createContext ("/redirect/307" , exchange -> {
100+ exchange .getResponseHeaders ().set ("Location" , "/post" );
101+ exchange .sendResponseHeaders (307 , -1 );
102+ exchange .getResponseBody ().close ();
103+ });
104+
105+ server .createContext ("/redirect/308" , exchange -> {
106+ exchange .getResponseHeaders ().set ("Location" , "/post" );
107+ exchange .sendResponseHeaders (308 , -1 );
108+ exchange .getResponseBody ().close ();
109+ });
110+
111+ server .createContext ("/redirect/circular" , exchange -> {
112+ exchange .getResponseHeaders ().set ("Location" , "/redirect/circular" );
113+ exchange .sendResponseHeaders (302 , -1 );
114+ exchange .getResponseBody ().close ();
115+ });
116+
117+ // /redirect/chain → 301 → /redirect/302 → 302 → /get
118+ server .createContext ("/redirect/chain" , exchange -> {
119+ exchange .getResponseHeaders ().set ("Location" , "/redirect/302" );
120+ exchange .sendResponseHeaders (301 , -1 );
121+ exchange .getResponseBody ().close ();
122+ });
123+
80124 server .start ();
81125 }
82126
@@ -331,4 +375,223 @@ void sendRequest_withDifferentHttpMethods_worksCorrectly() {
331375 assertNotNull (response );
332376 }
333377 }
378+
379+ // -------------------------------------------------------------------------
380+ // Redirect tests
381+ // -------------------------------------------------------------------------
382+
383+ @ Test
384+ void sendRequest_with301Redirect_followsRedirect () {
385+ JavaHttpRequest request = new JavaHttpRequest (
386+ baseUrl () + "/redirect/301" ,
387+ HttpMethod .GET ,
388+ null ,
389+ null ,
390+ false ,
391+ null
392+ );
393+
394+ ResponseEntity <String > response = clientService .sendRequest (request , new HttpHeaders ());
395+
396+ assertEquals (200 , response .getStatusCode ().value ());
397+ assertEquals ("GET Success" , response .getBody ());
398+
399+ // The redirect chain header must be present and mention status 301
400+ String chain = response .getHeaders ().getFirst ("X-Redirect-Chain" );
401+ assertNotNull (chain , "X-Redirect-Chain header should be present after a redirect" );
402+ assertTrue (chain .contains ("\" status\" :301" ),
403+ "Redirect chain should contain a step with status 301, got: " + chain );
404+ }
405+
406+ @ Test
407+ void sendRequest_with302Redirect_followsRedirect () {
408+ JavaHttpRequest request = new JavaHttpRequest (
409+ baseUrl () + "/redirect/302" ,
410+ HttpMethod .GET ,
411+ null ,
412+ null ,
413+ false ,
414+ null
415+ );
416+
417+ ResponseEntity <String > response = clientService .sendRequest (request , new HttpHeaders ());
418+
419+ assertEquals (200 , response .getStatusCode ().value ());
420+ assertEquals ("GET Success" , response .getBody ());
421+
422+ String chain = response .getHeaders ().getFirst ("X-Redirect-Chain" );
423+ assertNotNull (chain , "X-Redirect-Chain header should be present after a redirect" );
424+ assertTrue (chain .contains ("\" status\" :302" ),
425+ "Redirect chain should contain a step with status 302, got: " + chain );
426+ }
427+
428+ @ Test
429+ void sendRequest_with303Redirect_fromPost_becomeGet () {
430+ // POST to a 303 redirect; the 303 spec mandates the follow-up is always GET
431+ JavaHttpRequest request = new JavaHttpRequest (
432+ baseUrl () + "/redirect/303" ,
433+ HttpMethod .POST ,
434+ "{\" data\" :\" value\" }" ,
435+ "application/json" ,
436+ false ,
437+ null
438+ );
439+
440+ ResponseEntity <String > response = clientService .sendRequest (request , new HttpHeaders ());
441+
442+ // Final destination is /get which returns "GET Success"
443+ assertEquals (200 , response .getStatusCode ().value ());
444+ assertEquals ("GET Success" , response .getBody ());
445+
446+ String chain = response .getHeaders ().getFirst ("X-Redirect-Chain" );
447+ assertNotNull (chain , "X-Redirect-Chain header should be present after a 303 redirect" );
448+ assertTrue (chain .contains ("\" status\" :303" ),
449+ "Redirect chain should contain a step with status 303, got: " + chain );
450+ }
451+
452+ @ Test
453+ void sendRequest_with307Redirect_keepsPOSTMethod () {
454+ // 307 must keep the original method (POST) and body
455+ JavaHttpRequest request = new JavaHttpRequest (
456+ baseUrl () + "/redirect/307" ,
457+ HttpMethod .POST ,
458+ "hello-body" ,
459+ "text/plain" ,
460+ false ,
461+ null
462+ );
463+
464+ ResponseEntity <String > response = clientService .sendRequest (request , new HttpHeaders ());
465+
466+ // Final destination is /post which echoes the body
467+ assertEquals (200 , response .getStatusCode ().value ());
468+ assertNotNull (response .getBody ());
469+ assertTrue (response .getBody ().contains ("Received: hello-body" ),
470+ "307 redirect must preserve the POST body, got: " + response .getBody ());
471+
472+ String chain = response .getHeaders ().getFirst ("X-Redirect-Chain" );
473+ assertNotNull (chain , "X-Redirect-Chain header should be present after a 307 redirect" );
474+ assertTrue (chain .contains ("\" status\" :307" ),
475+ "Redirect chain should contain a step with status 307, got: " + chain );
476+ }
477+
478+ @ Test
479+ void sendRequest_withRedirectChain_capturesAllHops () {
480+ // /redirect/chain → 301 → /redirect/302 → 302 → /get (2 hops total)
481+ JavaHttpRequest request = new JavaHttpRequest (
482+ baseUrl () + "/redirect/chain" ,
483+ HttpMethod .GET ,
484+ null ,
485+ null ,
486+ false ,
487+ null
488+ );
489+
490+ ResponseEntity <String > response = clientService .sendRequest (request , new HttpHeaders ());
491+
492+ assertEquals (200 , response .getStatusCode ().value ());
493+ assertEquals ("GET Success" , response .getBody ());
494+
495+ String chain = response .getHeaders ().getFirst ("X-Redirect-Chain" );
496+ assertNotNull (chain , "X-Redirect-Chain header should be present after a redirect chain" );
497+
498+ // Two redirect steps must appear – count occurrences of "\"status\":"
499+ long hopCount = chain .chars ()
500+ .filter (c -> c == '{' )
501+ .count ();
502+ assertEquals (2 , hopCount ,
503+ "Redirect chain should capture exactly 2 hops for /redirect/chain, got chain: " + chain );
504+ }
505+
506+ @ Test
507+ void sendRequest_withCircularRedirect_stopsAtMaxRedirects () {
508+ // /redirect/circular points back to itself – ClientService must stop after MAX_REDIRECTS
509+ JavaHttpRequest request = new JavaHttpRequest (
510+ baseUrl () + "/redirect/circular" ,
511+ HttpMethod .GET ,
512+ null ,
513+ null ,
514+ false ,
515+ null
516+ );
517+
518+ // Must not throw and must return a response within a reasonable time
519+ ResponseEntity <String > response = assertDoesNotThrow (
520+ () -> clientService .sendRequest (request , new HttpHeaders ()),
521+ "Circular redirect must not cause an infinite loop or exception"
522+ );
523+
524+ assertNotNull (response , "Response must not be null for circular redirect" );
525+ // After MAX_REDIRECTS the service returns whatever the last response was (another 302)
526+ // or a 502 – either way it must be a valid HTTP status code
527+ assertTrue (response .getStatusCode ().value () >= 100 && response .getStatusCode ().value () < 600 ,
528+ "Response status must be a valid HTTP status code, got: " + response .getStatusCode ().value ());
529+ }
530+
531+ // -------------------------------------------------------------------------
532+ // Diagnostic header tests
533+ // -------------------------------------------------------------------------
534+
535+ @ Test
536+ void sendRequest_success_hasProtocolVersionHeader () {
537+ JavaHttpRequest request = new JavaHttpRequest (
538+ baseUrl () + "/get" ,
539+ HttpMethod .GET ,
540+ null ,
541+ null ,
542+ false ,
543+ null
544+ );
545+
546+ ResponseEntity <String > response = clientService .sendRequest (request , new HttpHeaders ());
547+
548+ assertEquals (200 , response .getStatusCode ().value ());
549+ String protocolVersion = response .getHeaders ().getFirst ("X-Protocol-Version" );
550+ assertNotNull (protocolVersion , "X-Protocol-Version header must be present" );
551+ // The embedded test server only speaks HTTP/1.1; the Java HttpClient may
552+ // negotiate HTTP/2 but will fall back to HTTP/1.1 for plain HTTP.
553+ assertTrue (
554+ protocolVersion .equals ("HTTP/1.1" ) || protocolVersion .equals ("HTTP/2" ),
555+ "X-Protocol-Version must be HTTP/1.1 or HTTP/2, got: " + protocolVersion
556+ );
557+ }
558+
559+ @ Test
560+ void sendRequest_success_hasResolvedIpHeader () {
561+ JavaHttpRequest request = new JavaHttpRequest (
562+ baseUrl () + "/get" ,
563+ HttpMethod .GET ,
564+ null ,
565+ null ,
566+ false ,
567+ null
568+ );
569+
570+ ResponseEntity <String > response = clientService .sendRequest (request , new HttpHeaders ());
571+
572+ assertEquals (200 , response .getStatusCode ().value ());
573+ String resolvedIp = response .getHeaders ().getFirst ("X-Resolved-IP" );
574+ assertNotNull (resolvedIp , "X-Resolved-IP header must be present when targeting localhost" );
575+ assertFalse (resolvedIp .isBlank (), "X-Resolved-IP header must not be empty" );
576+ }
577+
578+ @ Test
579+ void sendRequest_withNoRedirect_hasNoRedirectChainHeader () {
580+ JavaHttpRequest request = new JavaHttpRequest (
581+ baseUrl () + "/get" ,
582+ HttpMethod .GET ,
583+ null ,
584+ null ,
585+ false ,
586+ null
587+ );
588+
589+ ResponseEntity <String > response = clientService .sendRequest (request , new HttpHeaders ());
590+
591+ assertEquals (200 , response .getStatusCode ().value ());
592+ assertNull (
593+ response .getHeaders ().getFirst ("X-Redirect-Chain" ),
594+ "X-Redirect-Chain header must NOT be present when no redirect occurred"
595+ );
596+ }
334597}
0 commit comments