@@ -48,7 +48,12 @@ void setup() {
4848 @ Test
4949 void testFailoverForConfigFetch () throws Exception {
5050 // Use byte[]–based mocks since requestConfigsFromURI uses BodyHandlers.ofByteArray().
51- Prefab .Configs dummyConfigs = Prefab .Configs .newBuilder ().build ();
51+ Prefab .Configs dummyConfigs = Prefab .Configs
52+ .newBuilder ()
53+ .setConfigServicePointer (
54+ Prefab .ConfigServicePointer .newBuilder ().setProjectId (123L )
55+ )
56+ .build ();
5257 byte [] dummyBytes = dummyConfigs .toByteArray ();
5358
5459 HttpResponse <byte []> failureResponse = mock (HttpResponse .class );
@@ -128,7 +133,12 @@ void testFailoverForSSEConnection() throws Exception {
128133
129134 @ Test
130135 void testBasicCaching () throws Exception {
131- Prefab .Configs dummyConfigs = Prefab .Configs .newBuilder ().build ();
136+ Prefab .Configs dummyConfigs = Prefab .Configs
137+ .newBuilder ()
138+ .setConfigServicePointer (
139+ Prefab .ConfigServicePointer .newBuilder ().setProjectId (123L )
140+ )
141+ .build ();
132142 byte [] dummyBytes = dummyConfigs .toByteArray ();
133143
134144 HttpResponse <byte []> httpResponse200 = mock (HttpResponse .class );
@@ -175,7 +185,12 @@ void testBasicCaching() throws Exception {
175185 @ Test
176186 void testConditionalGet304 () throws Exception {
177187 // In order to trigger a conditional GET, we insert a cached entry that is expired.
178- Prefab .Configs dummyConfigs = Prefab .Configs .newBuilder ().build ();
188+ Prefab .Configs dummyConfigs = Prefab .Configs
189+ .newBuilder ()
190+ .setConfigServicePointer (
191+ Prefab .ConfigServicePointer .newBuilder ().setProjectId (123L )
192+ )
193+ .build ();
179194 byte [] dummyBytes = dummyConfigs .toByteArray ();
180195 // Use a time far enough in the past to ensure expiration.
181196 long past = System .currentTimeMillis () - 10_000 ;
@@ -225,7 +240,12 @@ void testConditionalGet304() throws Exception {
225240
226241 @ Test
227242 void testClearCache () throws Exception {
228- Prefab .Configs dummyConfigs = Prefab .Configs .newBuilder ().build ();
243+ Prefab .Configs dummyConfigs = Prefab .Configs
244+ .newBuilder ()
245+ .setConfigServicePointer (
246+ Prefab .ConfigServicePointer .newBuilder ().setProjectId (123L )
247+ )
248+ .build ();
229249 byte [] dummyBytes = dummyConfigs .toByteArray ();
230250
231251 HttpResponse <byte []> httpResponse200 = mock (HttpResponse .class );
@@ -280,7 +300,12 @@ void testClearCache() throws Exception {
280300 @ Test
281301 void testNoCacheResponseAlwaysRevalidates () throws Exception {
282302 // Create a valid Prefab.Configs instance and its serialized form.
283- Prefab .Configs dummyConfigs = Prefab .Configs .newBuilder ().build ();
303+ Prefab .Configs dummyConfigs = Prefab .Configs
304+ .newBuilder ()
305+ .setConfigServicePointer (
306+ Prefab .ConfigServicePointer .newBuilder ().setProjectId (123L )
307+ )
308+ .build ();
284309 byte [] dummyBytes = dummyConfigs .toByteArray ();
285310
286311 // Simulate a 200 response with Cache-Control: no-cache and an ETag.
@@ -365,4 +390,170 @@ void testNoCacheResponseAlwaysRevalidates() throws Exception {
365390 assertThat (sentRequest .headers ().firstValue ("If-None-Match" ))
366391 .contains ("etag-no-cache" );
367392 }
393+
394+ @ Test
395+ void testZeroByteConfigRejectionFromHttpResponse () throws Exception {
396+ // Mock a 200 response that returns zero bytes
397+ byte [] zeroBytes = new byte [0 ];
398+ HttpResponse <byte []> zeroByteResponse = mock (HttpResponse .class );
399+ when (zeroByteResponse .statusCode ()).thenReturn (200 );
400+ when (zeroByteResponse .body ()).thenReturn (zeroBytes );
401+ when (zeroByteResponse .headers ()).thenReturn (HttpHeaders .of (Map .of (), (k , v ) -> true ));
402+
403+ CompletableFuture <HttpResponse <byte []>> futureZeroBytes = CompletableFuture .completedFuture (
404+ zeroByteResponse
405+ );
406+ when (
407+ mockHttpClient .sendAsync (
408+ any (HttpRequest .class ),
409+ any (HttpResponse .BodyHandler .class )
410+ )
411+ )
412+ .thenReturn (futureZeroBytes );
413+
414+ // Request configs - this should eventually fail after retries
415+ CompletableFuture <HttpResponse <Supplier <Prefab .Configs >>> result = prefabHttpClient .requestConfigs (
416+ 0L
417+ );
418+
419+ HttpResponse <Supplier <Prefab .Configs >> response = result .get ();
420+ assertThat (response .statusCode ()).isEqualTo (200 );
421+
422+ // Try to get the body - this should throw IllegalArgumentException
423+ try {
424+ response .body ().get ();
425+ assertThat (false )
426+ .as ("Expected IllegalArgumentException for zero-byte config" )
427+ .isTrue ();
428+ } catch (IllegalArgumentException e ) {
429+ // Should get IllegalArgumentException from zero-byte rejection
430+ assertThat (e .getMessage ()).contains ("Zero-byte config data is not valid" );
431+ }
432+ }
433+
434+ @ Test
435+ void testZeroByteConfigRejectionFromNon200Response () throws Exception {
436+ // Mock a 404 response that returns zero bytes
437+ byte [] zeroBytes = new byte [0 ];
438+ HttpResponse <byte []> zeroByteResponse = mock (HttpResponse .class );
439+ when (zeroByteResponse .statusCode ()).thenReturn (404 );
440+ when (zeroByteResponse .body ()).thenReturn (zeroBytes );
441+ when (zeroByteResponse .headers ()).thenReturn (HttpHeaders .of (Map .of (), (k , v ) -> true ));
442+
443+ CompletableFuture <HttpResponse <byte []>> futureZeroBytes = CompletableFuture .completedFuture (
444+ zeroByteResponse
445+ );
446+ when (
447+ mockHttpClient .sendAsync (
448+ any (HttpRequest .class ),
449+ any (HttpResponse .BodyHandler .class )
450+ )
451+ )
452+ .thenReturn (futureZeroBytes );
453+
454+ // Request configs - this should eventually fail after retries
455+ CompletableFuture <HttpResponse <Supplier <Prefab .Configs >>> result = prefabHttpClient .requestConfigs (
456+ 0L
457+ );
458+
459+ HttpResponse <Supplier <Prefab .Configs >> response = result .get ();
460+ assertThat (response .statusCode ()).isEqualTo (404 );
461+
462+ // Try to get the body - this should throw IllegalArgumentException
463+ try {
464+ response .body ().get ();
465+ assertThat (false )
466+ .as ("Expected IllegalArgumentException for zero-byte config" )
467+ .isTrue ();
468+ } catch (IllegalArgumentException e ) {
469+ // Should get IllegalArgumentException from zero-byte rejection
470+ assertThat (e .getMessage ()).contains ("Zero-byte config data is not valid" );
471+ }
472+ }
473+
474+ @ Test
475+ void testZeroByteConfigRejectionFromCache () throws Exception {
476+ // Insert zero-byte data directly into cache
477+ URI uri = URI .create ("http://a.example.com/api/v2/configs/0" );
478+ Field cacheField = HttpClient .class .getDeclaredField ("configCache" );
479+ cacheField .setAccessible (true );
480+ @ SuppressWarnings ("unchecked" )
481+ Cache <URI , HttpClient .CacheEntry > cache = (Cache <URI , HttpClient .CacheEntry >) cacheField .get (
482+ prefabHttpClient
483+ );
484+
485+ // Create a cache entry with zero-byte data that is still fresh
486+ long future = System .currentTimeMillis () + 60_000 ;
487+ HttpClient .CacheEntry zeroByteCacheEntry = new HttpClient .CacheEntry (
488+ new byte [0 ],
489+ "zero-byte-etag" ,
490+ future
491+ );
492+ cache .put (uri , zeroByteCacheEntry );
493+
494+ // Request configs - should return cached response but fail when accessing body
495+ CompletableFuture <HttpResponse <Supplier <Prefab .Configs >>> result = prefabHttpClient .requestConfigs (
496+ 0L
497+ );
498+
499+ HttpResponse <Supplier <Prefab .Configs >> response = result .get ();
500+ assertThat (response .statusCode ()).isEqualTo (200 );
501+ assertThat (response .headers ().firstValue ("X-Cache" )).contains ("HIT" );
502+
503+ // Try to get the body - this should throw IllegalArgumentException
504+ try {
505+ response .body ().get ();
506+ assertThat (false )
507+ .as ("Expected IllegalArgumentException for zero-byte cached config" )
508+ .isTrue ();
509+ } catch (IllegalArgumentException e ) {
510+ // Should get IllegalArgumentException from zero-byte rejection
511+ assertThat (e .getMessage ()).contains ("Zero-byte config data is not valid" );
512+ }
513+ }
514+
515+ @ Test
516+ void testValidConfigProcessingAfterZeroByteRejectionImplementation () throws Exception {
517+ // This test verifies that valid configs can still be processed normally
518+ // even when zero-byte rejection is in place
519+
520+ Prefab .Configs validConfigs = Prefab .Configs
521+ .newBuilder ()
522+ .setConfigServicePointer (
523+ Prefab .ConfigServicePointer .newBuilder ().setProjectId (456L )
524+ )
525+ .build ();
526+ byte [] validBytes = validConfigs .toByteArray ();
527+ HttpResponse <byte []> validResponse = mock (HttpResponse .class );
528+ when (validResponse .statusCode ()).thenReturn (200 );
529+ when (validResponse .body ()).thenReturn (validBytes );
530+ when (validResponse .headers ()).thenReturn (HttpHeaders .of (Map .of (), (k , v ) -> true ));
531+
532+ CompletableFuture <HttpResponse <byte []>> futureValidBytes = CompletableFuture .completedFuture (
533+ validResponse
534+ );
535+
536+ when (
537+ mockHttpClient .sendAsync (
538+ any (HttpRequest .class ),
539+ any (HttpResponse .BodyHandler .class )
540+ )
541+ )
542+ .thenReturn (futureValidBytes );
543+
544+ // Request configs - should succeed with valid data
545+ CompletableFuture <HttpResponse <Supplier <Prefab .Configs >>> result = prefabHttpClient .requestConfigs (
546+ 0L
547+ );
548+
549+ HttpResponse <Supplier <Prefab .Configs >> response = result .get ();
550+ assertThat (response .statusCode ()).isEqualTo (200 );
551+
552+ // Should be able to get valid configs without exception
553+ Prefab .Configs configs = response .body ().get ();
554+ assertThat (configs ).isEqualTo (validConfigs );
555+
556+ // Should have called sendAsync once
557+ verify (mockHttpClient , times (1 )).sendAsync (any (), any ());
558+ }
368559}
0 commit comments