|
23 | 23 | import static org.hamcrest.Matchers.hasKey; |
24 | 24 | import static org.hamcrest.Matchers.instanceOf; |
25 | 25 | import static org.hamcrest.Matchers.notNullValue; |
| 26 | +import static org.junit.jupiter.api.Assertions.assertThrows; |
26 | 27 | import static org.mockito.ArgumentMatchers.any; |
27 | 28 | import static org.mockito.Mockito.atLeastOnce; |
28 | 29 | import static org.mockito.Mockito.doAnswer; |
| 30 | +import static org.mockito.Mockito.doThrow; |
29 | 31 | import static org.mockito.Mockito.verify; |
30 | 32 | import static org.mockito.Mockito.when; |
31 | 33 | import static org.testcontainers.shaded.org.awaitility.Awaitility.await; |
|
37 | 39 | import java.util.concurrent.ConcurrentHashMap; |
38 | 40 | import java.util.concurrent.ConcurrentMap; |
39 | 41 | import java.util.concurrent.ExecutorService; |
| 42 | +import java.util.concurrent.RejectedExecutionException; |
40 | 43 | import java.util.concurrent.TimeUnit; |
41 | 44 | import java.util.concurrent.atomic.AtomicInteger; |
42 | 45 | import java.util.function.BiFunction; |
|
46 | 49 | import org.junit.jupiter.api.BeforeEach; |
47 | 50 | import org.junit.jupiter.api.Test; |
48 | 51 | import org.junit.jupiter.api.extension.ExtendWith; |
| 52 | +import org.mockito.ArgumentCaptor; |
49 | 53 | import org.mockito.Mock; |
50 | 54 | import org.mockito.Mock.Strictness; |
51 | 55 | import org.mockito.junit.jupiter.MockitoExtension; |
52 | 56 |
|
53 | 57 | import com.amazon.sns.messaging.lib.concurrent.RingBufferBlockingQueue; |
54 | 58 | import com.amazon.sns.messaging.lib.core.RequestEntryInternalFactory.RequestEntryInternal; |
| 59 | +import com.amazon.sns.messaging.lib.exception.PoisonRequestEntryException; |
55 | 60 | import com.amazon.sns.messaging.lib.helpers.TryConsumer; |
56 | 61 | import com.amazon.sns.messaging.lib.model.RequestEntry; |
57 | 62 | import com.amazon.sns.messaging.lib.model.ResponseFailEntry; |
@@ -112,6 +117,42 @@ void testConstructorInitializesTopicRequests() { |
112 | 117 | assertThat(topicRequests.isEmpty(), is(true)); |
113 | 118 | } |
114 | 119 |
|
| 120 | + @Test |
| 121 | + void testConstructorThrowsNpeWhenTopicPropertyIsNull() { |
| 122 | + final NullPointerException thrown = assertThrows(NullPointerException.class, () -> |
| 123 | + new TestableAmazonSnsConsumer(amazonSnsClient, null, objectMapper, pendingRequests, topicRequests, executorService, publishDecorator) |
| 124 | + ); |
| 125 | + |
| 126 | + assertThat(thrown.getMessage(), containsString("topicProperty cannot be null")); |
| 127 | + } |
| 128 | + |
| 129 | + @Test |
| 130 | + void testConstructorThrowsNpeWhenAmazonSnsClientIsNull() { |
| 131 | + final NullPointerException thrown = assertThrows(NullPointerException.class, () -> |
| 132 | + new TestableAmazonSnsConsumer(null, topicProperty, objectMapper, pendingRequests, topicRequests, executorService, publishDecorator) |
| 133 | + ); |
| 134 | + |
| 135 | + assertThat(thrown.getMessage(), containsString("amazonSnsClient cannot be null")); |
| 136 | + } |
| 137 | + |
| 138 | + @Test |
| 139 | + void testConstructorThrowsNpeWhenObjectMapperIsNull() { |
| 140 | + final NullPointerException thrown = assertThrows(NullPointerException.class, () -> |
| 141 | + new TestableAmazonSnsConsumer(amazonSnsClient, topicProperty, null, pendingRequests, topicRequests, executorService, publishDecorator) |
| 142 | + ); |
| 143 | + |
| 144 | + assertThat(thrown.getMessage(), containsString("objectMapper cannot be null")); |
| 145 | + } |
| 146 | + |
| 147 | + @Test |
| 148 | + void testConstructorThrowsNpeWhenExecutorServiceIsNull() { |
| 149 | + final NullPointerException thrown = assertThrows(NullPointerException.class, () -> |
| 150 | + new TestableAmazonSnsConsumer(amazonSnsClient, topicProperty, objectMapper, pendingRequests, topicRequests, null, publishDecorator) |
| 151 | + ); |
| 152 | + |
| 153 | + assertThat(thrown.getMessage(), containsString("executorService cannot be null")); |
| 154 | + } |
| 155 | + |
115 | 156 | @Test |
116 | 157 | void testAwaitReturnCompletableFutureWhenQueuesAreEmpty() throws Exception { |
117 | 158 | context(consumer -> { |
@@ -251,6 +292,22 @@ void testRunRecordsCorrectExceptionOnPublishFailure() throws Exception { |
251 | 292 | }); |
252 | 293 | } |
253 | 294 |
|
| 295 | + @Test |
| 296 | + void testRunHandlesRejectedExecutionExceptionFromExecutorWithoutCrashing() throws Exception { |
| 297 | + when(topicProperty.isFifo()).thenReturn(false); |
| 298 | + doThrow(new RejectedExecutionException("executor full")).when(executorService).execute(any(Runnable.class)); |
| 299 | + |
| 300 | + context(consumer -> { |
| 301 | + topicRequests.put(buildRequestEntry("rejected-message")); |
| 302 | + |
| 303 | + await() |
| 304 | + .untilAsserted(() -> { |
| 305 | + assertThat(consumer.getHandleErrorCallCount(), greaterThanOrEqualTo(1)); |
| 306 | + assertThat(consumer.getLastError(), instanceOf(RejectedExecutionException.class)); |
| 307 | + }); |
| 308 | + }); |
| 309 | + } |
| 310 | + |
254 | 311 | @Test |
255 | 312 | void testRunDoesNotPublishWhenQueueIsEmpty() throws Exception { |
256 | 313 | context(consumer -> { |
@@ -494,6 +551,38 @@ void testCanAddPayloadSplitsBatchWhenCumulativeSizeExceedsThreshold() throws Exc |
494 | 551 | }); |
495 | 552 | } |
496 | 553 |
|
| 554 | + @Test |
| 555 | + void testPoisonRequestEntryRemovesFromPendingRequestsAndFailsListenableFuture() throws Exception { |
| 556 | + when(topicProperty.isFifo()).thenReturn(true); |
| 557 | + |
| 558 | + final String poisonId = "poison-id"; |
| 559 | + final String oversizedPayload = buildPayloadOfBytes(TestableAmazonSnsConsumer.batchSizeBytesThreshold() + 100); |
| 560 | + final RequestEntry<String> poisonEntry = RequestEntry.<String>builder() |
| 561 | + .withId(poisonId) |
| 562 | + .withValue(oversizedPayload) |
| 563 | + .build(); |
| 564 | + |
| 565 | + pendingRequests.put(poisonId, listenableFutureImpl); |
| 566 | + |
| 567 | + context(consumer -> { |
| 568 | + topicRequests.put(poisonEntry); |
| 569 | + |
| 570 | + await() |
| 571 | + .untilAsserted(() -> { |
| 572 | + assertThat(pendingRequests.containsKey(poisonId), is(false)); |
| 573 | + |
| 574 | + final ArgumentCaptor<ResponseFailEntry> captor = ArgumentCaptor.forClass(ResponseFailEntry.class); |
| 575 | + verify(listenableFutureImpl, atLeastOnce()).fail(captor.capture()); |
| 576 | + |
| 577 | + final ResponseFailEntry failEntry = captor.getValue(); |
| 578 | + assertThat(failEntry.getId(), is(poisonId)); |
| 579 | + assertThat(failEntry.getCode(), is("000")); |
| 580 | + assertThat(failEntry.getSenderFault(), is(true)); |
| 581 | + assertThat(failEntry.getThrowable(), instanceOf(PoisonRequestEntryException.class)); |
| 582 | + }); |
| 583 | + }); |
| 584 | + } |
| 585 | + |
497 | 586 | @Test |
498 | 587 | void testCanAddPayloadDoesNotPublishEmptyBatchWhenAllEntriesExceedThreshold() throws Exception { |
499 | 588 | when(topicProperty.isFifo()).thenReturn(true); |
|
0 commit comments