|
10 | 10 | import org.junit.Test; |
11 | 11 |
|
12 | 12 | import java.io.IOException; |
| 13 | +import java.util.Map; |
13 | 14 | import java.util.concurrent.CountDownLatch; |
| 15 | +import java.util.concurrent.ExecutorService; |
| 16 | +import java.util.concurrent.Executors; |
| 17 | +import java.util.concurrent.TimeUnit; |
| 18 | +import java.util.concurrent.atomic.AtomicReference; |
14 | 19 |
|
15 | 20 | import okhttp3.Call; |
16 | 21 | import okhttp3.Headers; |
17 | 22 | import okhttp3.Interceptor; |
18 | 23 | import okhttp3.OkHttpClient; |
19 | 24 | import okhttp3.Response; |
| 25 | +import okhttp3.mockwebserver.MockResponse; |
20 | 26 | import okhttp3.mockwebserver.RecordedRequest; |
21 | 27 |
|
22 | 28 | import static com.contentful.java.cda.SyncType.onlyEntriesOfType; |
@@ -353,6 +359,40 @@ public void requestingWhileRateLimitedThrows() { |
353 | 359 | } |
354 | 360 | } |
355 | 361 |
|
| 362 | + // Regression test: response.body() can be null for a body-less unsuccessful response (e.g. an |
| 363 | + // upstream proxy returning a bare 500/502 with no content). readResponseBody() in |
| 364 | + // CDAHttpException used to dereference response.body() without a null check, throwing an |
| 365 | + // unrelated NullPointerException that masked the real HTTP error. HTTP 204/304 are treated as |
| 366 | + // successful by Retrofit/OkHttp and never reach CDAHttpException, so this is reproduced via a |
| 367 | + // body-less 5xx error response instead. |
| 368 | + @Test |
| 369 | + @Enqueue |
| 370 | + public void bodyLess500ResponseDoesNotThrowNpeAndProducesMeaningfulException() { |
| 371 | + server.enqueue(new MockResponse().setResponseCode(500)); |
| 372 | + |
| 373 | + try { |
| 374 | + client.fetch(CDAEntry.class).all(); |
| 375 | + throw new AssertionError("Expected CDAHttpException to be thrown"); |
| 376 | + } catch (CDAHttpException cdaException) { |
| 377 | + assertThat(cdaException.responseCode()).isEqualTo(500); |
| 378 | + assertThat(cdaException.responseBody()).isNotNull(); |
| 379 | + } |
| 380 | + } |
| 381 | + |
| 382 | + @Test |
| 383 | + @Enqueue |
| 384 | + public void bodyLess502ResponseDoesNotThrowNpeAndProducesMeaningfulException() { |
| 385 | + server.enqueue(new MockResponse().setResponseCode(502)); |
| 386 | + |
| 387 | + try { |
| 388 | + client.fetch(CDAEntry.class).all(); |
| 389 | + throw new AssertionError("Expected CDAHttpException to be thrown"); |
| 390 | + } catch (CDAHttpException cdaException) { |
| 391 | + assertThat(cdaException.responseCode()).isEqualTo(502); |
| 392 | + assertThat(cdaException.responseBody()).isNotNull(); |
| 393 | + } |
| 394 | + } |
| 395 | + |
356 | 396 | @Test(expected = IllegalArgumentException.class) |
357 | 397 | @Enqueue("demo/content_types_cat.json") |
358 | 398 | public void settingNoLoggerAndAnyLogLevelResultsException() { |
@@ -381,6 +421,87 @@ public void clearingTheCacheClearsTheCache() { |
381 | 421 | assertThat(client.cache.locales()).isNull(); |
382 | 422 | } |
383 | 423 |
|
| 424 | + // Regression test: cache.types() returns null right after clearCache() is called. |
| 425 | + // CDAClient#cacheTypeWithId(String) and the content-type caching step in |
| 426 | + // ObserveQuery#one(String) used to dereference that null directly, causing an unhandled |
| 427 | + // NullPointerException. |
| 428 | + @Test |
| 429 | + @Enqueue(defaults = {}, value = { |
| 430 | + "demo/locales.json", |
| 431 | + "demo/content_types_cat.json", |
| 432 | + "demo/content_types_cat.json", |
| 433 | + "demo/locales.json", |
| 434 | + "demo/content_types_cat.json", |
| 435 | + "demo/content_types_cat.json" |
| 436 | + }) |
| 437 | + public void cacheTypeWithIdAfterClearCacheDoesNotThrow() { |
| 438 | + // populate the cache first |
| 439 | + client.fetch(CDAContentType.class).all(); |
| 440 | + assertThat(client.cache.types()).isNotNull(); |
| 441 | + |
| 442 | + client.clearCache(); |
| 443 | + // Preserve the existing, tested contract: cache.types() is null right after clear(). |
| 444 | + assertThat(client.cache.types()).isNull(); |
| 445 | + |
| 446 | + CDAContentType result = client.cacheTypeWithId("cat").blockingFirst(); |
| 447 | + |
| 448 | + assertThat(result).isNotNull(); |
| 449 | + assertThat(result.id()).isEqualTo("cat"); |
| 450 | + } |
| 451 | + |
| 452 | + // Regression test: concurrent Cache#clear() and Cache#types() reads must never throw, since |
| 453 | + // clearCache() is a public API that can legitimately race with any in-flight observable chain. |
| 454 | + @Test |
| 455 | + public void concurrentClearAndReadOfCacheTypesDoesNotThrow() throws InterruptedException { |
| 456 | + final Cache cache = new Cache(); |
| 457 | + final int iterations = 500; |
| 458 | + final CountDownLatch start = new CountDownLatch(1); |
| 459 | + final AtomicReference<Throwable> failure = new AtomicReference<>(); |
| 460 | + ExecutorService executor = Executors.newFixedThreadPool(2); |
| 461 | + |
| 462 | + Runnable clearer = () -> { |
| 463 | + try { |
| 464 | + start.await(); |
| 465 | + for (int i = 0; i < iterations; i++) { |
| 466 | + cache.clear(); |
| 467 | + } |
| 468 | + } catch (Throwable t) { |
| 469 | + failure.compareAndSet(null, t); |
| 470 | + } |
| 471 | + }; |
| 472 | + |
| 473 | + Runnable reader = () -> { |
| 474 | + try { |
| 475 | + start.await(); |
| 476 | + for (int i = 0; i < iterations; i++) { |
| 477 | + Map<String, CDAContentType> types = cache.types(); |
| 478 | + if (types != null) { |
| 479 | + types.get("any-id"); |
| 480 | + } |
| 481 | + } |
| 482 | + } catch (Throwable t) { |
| 483 | + failure.compareAndSet(null, t); |
| 484 | + } |
| 485 | + }; |
| 486 | + |
| 487 | + executor.submit(clearer); |
| 488 | + executor.submit(reader); |
| 489 | + start.countDown(); |
| 490 | + |
| 491 | + executor.shutdown(); |
| 492 | + executor.awaitTermination(5, TimeUnit.SECONDS); |
| 493 | + |
| 494 | + assertThat(failure.get()).isNull(); |
| 495 | + } |
| 496 | + |
| 497 | + // Regression test: the builder used to default logSensitiveData to `true`, leaking |
| 498 | + // authorization headers into CDAHttpException#toString() output by default. |
| 499 | + @Test |
| 500 | + public void logSensitiveDataDefaultsToFalse() { |
| 501 | + CDAClient defaultClient = createBuilder().build(); |
| 502 | + assertThat(defaultClient.shouldLogSensitiveData()).isFalse(); |
| 503 | + } |
| 504 | + |
384 | 505 | @Test |
385 | 506 | @Enqueue("demo/content_types_cat.json") |
386 | 507 | public void localesGetCached() { |
|
0 commit comments