diff --git a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/exception/MaximumAllowedMessageException.java b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/exception/MaximumAllowedMessageException.java index a2259dd..3dbc52e 100644 --- a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/exception/MaximumAllowedMessageException.java +++ b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/exception/MaximumAllowedMessageException.java @@ -25,7 +25,7 @@ * SNS. Contains the offending {@link RequestEntry} for diagnostic purposes. */ @Getter -@SuppressWarnings({ "rawtypes", "unchecked" }) +@SuppressWarnings({ "rawtypes", "unchecked", "java:S1948" }) public class MaximumAllowedMessageException extends RuntimeException { private static final long serialVersionUID = -529663449633021689L; diff --git a/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/core/RequestEntryInternalFactoryTest.java b/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/core/RequestEntryInternalFactoryTest.java new file mode 100644 index 0000000..0d53fb5 --- /dev/null +++ b/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/core/RequestEntryInternalFactoryTest.java @@ -0,0 +1,622 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.sns.messaging.lib.core; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.amazon.sns.messaging.lib.core.RequestEntryInternalFactory.MessageAttributesInternal; +import com.amazon.sns.messaging.lib.core.RequestEntryInternalFactory.RequestEntryInternal; +import com.amazon.sns.messaging.lib.model.RequestEntry; +import com.fasterxml.jackson.databind.ObjectMapper; + +// @formatter:off +class RequestEntryInternalFactoryTest { + + private ObjectMapper objectMapper; + private RequestEntryInternalFactory factory; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + factory = new RequestEntryInternalFactory(objectMapper); + } + + private RequestEntry buildRequestEntry(final Object payload, final Map headers) { + return RequestEntry.builder() + .withValue(payload) + .withId("test-id") + .withSubject("test-subject") + .withGroupId("group-1") + .withDeduplicationId("dedup-1") + .withMessageHeaders(headers) + .build(); + } + + private RequestEntry buildMinimalRequestEntry(final Object payload) { + return buildRequestEntry(payload, new HashMap<>()); + } + + @Nested + class CreateWithBytes { + + @Test + void testCreateWithBytesReturnsNotNull() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result, is(notNullValue())); + } + + @Test + void testCreateWithBytesMapsId() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getId(), equalTo("test-id")); + } + + @Test + void testCreateWithBytesMapsSubject() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getSubject(), equalTo("test-subject")); + } + + @Test + void testCreateWithBytesMapsGroupId() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getGroupId(), equalTo("group-1")); + } + + @Test + void testCreateWithBytesMapsDeduplicationId() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getDeduplicationId(), equalTo("dedup-1")); + } + + @Test + void testCreateWithBytesMapsMessageHeaders() { + final Map headers = new HashMap<>(); + headers.put("headerKey", "headerValue"); + final RequestEntry entry = buildRequestEntry("hello", headers); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getMessageHeaders(), equalTo(headers)); + } + + @Test + void testCreateWithBytesPayloadSizeMatchesByteArrayLength() { + final byte[] bytes = "hello world".getBytes(StandardCharsets.UTF_8); + final RequestEntry entry = buildMinimalRequestEntry("hello world"); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.size(), equalTo(bytes.length)); + } + + @Test + void testCreateWithBytesPayloadDecodedCorrectly() { + final String message = "hello world"; + final byte[] bytes = message.getBytes(StandardCharsets.UTF_8); + final RequestEntry entry = buildMinimalRequestEntry(message); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getMessage(), equalTo(message)); + } + + @Test + void testCreateWithBytesCreateTimeIsSet() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getCreateTime(), greaterThan(0L)); + } + + @Test + void testCreateWithEmptyBytesPayloadSizeIsZero() { + final byte[] bytes = new byte[0]; + final RequestEntry entry = buildMinimalRequestEntry(""); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.size(), equalTo(0)); + } + + @Test + void testCreateWithNullHeadersMapsNullHeaders() { + final RequestEntry entry = buildRequestEntry("hello", null); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getMessageHeaders(), is(nullValue())); + } + } + + @Nested + class CreateAutoSerialize { + + @Test + void testCreateWithStringPayloadReturnsNotNull() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result, is(notNullValue())); + } + + @Test + void testCreateWithStringPayloadDecodesCorrectly() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result.getMessage(), equalTo("hello")); + } + + @Test + void testCreateWithStringPayloadSizeMatchesUtf8Length() { + final String message = "hello"; + final RequestEntry entry = buildMinimalRequestEntry(message); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result.size(), equalTo(message.getBytes(StandardCharsets.UTF_8).length)); + } + + @Test + void testCreateWithMultibyteStringPayloadEncodedInUtf8() { + final String message = "こんにちは"; + final RequestEntry entry = buildMinimalRequestEntry(message); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result.size(), equalTo(message.getBytes(StandardCharsets.UTF_8).length)); + } + + @Test + void testCreateWithObjectPayloadSerializesViaJackson() throws Exception { + final Map payload = new HashMap<>(); + payload.put("key", "value"); + final RequestEntry entry = buildMinimalRequestEntry(payload); + + final RequestEntryInternal result = factory.create(entry); + + final String decoded = result.getMessage(); + final Map parsed = objectMapper.readValue(decoded, Map.class); + assertThat(parsed.get("key"), equalTo("value")); + } + + @Test + void testCreateWithIntegerPayloadSerializesViaJackson() throws Exception { + final RequestEntry entry = buildMinimalRequestEntry(42); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result.getMessage(), equalTo("42")); + } + + @Test + void testCreateWithEmptyStringPayloadSizeIsZero() { + final RequestEntry entry = buildMinimalRequestEntry(""); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result.size(), equalTo(0)); + } + + @Test + void testCreateWithStringPayloadMapsId() { + final RequestEntry entry = buildMinimalRequestEntry("payload"); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result.getId(), equalTo("test-id")); + } + } + + @Nested + class ConvertPayload { + + @Test + void testConvertPayloadStringReturnsUtf8Bytes() { + final String value = "hello"; + final RequestEntry entry = buildMinimalRequestEntry(value); + + final byte[] result = factory.convertPayload(entry); + + assertThat(result, equalTo(value.getBytes(StandardCharsets.UTF_8))); + } + + @Test + void testConvertPayloadStringIsNotSerializedWithJacksonQuotes() { + final String value = "hello"; + final RequestEntry entry = buildMinimalRequestEntry(value); + + final byte[] result = factory.convertPayload(entry); + + assertThat(new String(result, StandardCharsets.UTF_8), equalTo("hello")); + } + + @Test + void testConvertPayloadObjectUsesJackson() throws Exception { + final Map payload = Collections.singletonMap("a", "b"); + final RequestEntry entry = buildMinimalRequestEntry(payload); + + final byte[] result = factory.convertPayload(entry); + + final Map parsed = objectMapper.readValue(result, Map.class); + assertThat(parsed.get("a"), equalTo("b")); + } + + @Test + void testConvertPayloadListUsesJackson() throws Exception { + final List payload = Arrays.asList("x", "y", "z"); + final RequestEntry entry = buildMinimalRequestEntry(payload); + + final byte[] result = factory.convertPayload(entry); + + final List parsed = objectMapper.readValue(result, List.class); + assertThat(parsed.size(), equalTo(3)); + } + + @Test + void testConvertPayloadMultibyteStringEncodedCorrectly() { + final String value = "日本語"; + final RequestEntry entry = buildMinimalRequestEntry(value); + + final byte[] result = factory.convertPayload(entry); + + assertThat(result, equalTo(value.getBytes(StandardCharsets.UTF_8))); + } + + @Test + void testConvertPayloadEmptyStringReturnsEmptyArray() { + final RequestEntry entry = buildMinimalRequestEntry(""); + + final byte[] result = factory.convertPayload(entry); + + assertThat(result.length, equalTo(0)); + } + } + + @Nested + class MessageAttributesSize { + + @Test + void testMessageAttributesSizeWithEmptyHeadersReturnsZero() { + final RequestEntry entry = buildRequestEntry("hello", new HashMap<>()); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, equalTo(0)); + } + + @Test + void testMessageAttributesSizeWithStringAttributeCountsKeyAndValue() { + final Map headers = new HashMap<>(); + headers.put("key", "value"); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, equalTo(8)); + } + + @Test + void testMessageAttributesSizeWithMultipleAttributesSumsAll() { + final Map headers = new HashMap<>(); + headers.put("k1", "val"); + headers.put("k2", "valu"); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, equalTo(11)); + } + + @Test + void testMessageAttributesSizeWithNumberAttribute() { + final Map headers = new HashMap<>(); + headers.put("num", 12345); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, greaterThan(0)); + } + + @Test + void testMessageAttributesSizeWithEnumAttribute() { + final Map headers = new HashMap<>(); + headers.put("e", SampleEnum.VALUE_ONE); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, equalTo(10)); + } + + @Test + void testMessageAttributesSizeWithBinaryAttribute() { + final byte[] bytes = new byte[] { 1, 2, 3, 4 }; + final Map headers = new HashMap<>(); + headers.put("bin", ByteBuffer.wrap(bytes)); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, equalTo(7)); + } + + @Test + void testMessageAttributesSizeWithStringListAttribute() { + final Map headers = new HashMap<>(); + headers.put("arr", Arrays.asList("a", "b", "c")); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, greaterThan(0)); + } + + @Test + void testMessageAttributesSizeIsNonNegative() { + final Map headers = new HashMap<>(); + headers.put("x", "y"); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, greaterThanOrEqualTo(0)); + } + } + + @Nested + class RequestEntryInternalTest { + + @Test + void testSizeReturnsBufferCapacity() { + final byte[] bytes = "test payload".getBytes(StandardCharsets.UTF_8); + final RequestEntryInternal internal = RequestEntryInternal.builder() + .withId("id") + .withValue(ByteBuffer.wrap(bytes)) + .withCreateTime(System.nanoTime()) + .build(); + + assertThat(internal.size(), equalTo(bytes.length)); + } + + @Test + void testGetMessageDecodesUtf8Correctly() { + final String message = "decoded message"; + final RequestEntryInternal internal = RequestEntryInternal.builder() + .withId("id") + .withValue(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8))) + .withCreateTime(System.nanoTime()) + .build(); + + assertThat(internal.getMessage(), equalTo(message)); + } + + @Test + void testGetMessageDecodesMultibyteUtf8Correctly() { + final String message = "日本語テスト"; + final RequestEntryInternal internal = RequestEntryInternal.builder() + .withId("id") + .withValue(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8))) + .withCreateTime(System.nanoTime()) + .build(); + + assertThat(internal.getMessage(), equalTo(message)); + } + + @Test + void testBuilderSetsAllFieldsCorrectly() { + final Map headers = Collections.singletonMap("h", "v"); + final long now = System.nanoTime(); + final RequestEntryInternal internal = RequestEntryInternal.builder() + .withId("my-id") + .withSubject("my-subject") + .withGroupId("my-group") + .withDeduplicationId("my-dedup") + .withMessageHeaders(headers) + .withValue(ByteBuffer.wrap("data".getBytes(StandardCharsets.UTF_8))) + .withCreateTime(now) + .build(); + + assertThat(internal.getId(), equalTo("my-id")); + assertThat(internal.getSubject(), equalTo("my-subject")); + assertThat(internal.getGroupId(), equalTo("my-group")); + assertThat(internal.getDeduplicationId(), equalTo("my-dedup")); + assertThat(internal.getMessageHeaders(), equalTo(headers)); + assertThat(internal.getCreateTime(), equalTo(now)); + } + + @Test + void testSizeWithEmptyBufferIsZero() { + final RequestEntryInternal internal = RequestEntryInternal.builder() + .withId("id") + .withValue(ByteBuffer.wrap(new byte[0])) + .withCreateTime(System.nanoTime()) + .build(); + + assertThat(internal.size(), equalTo(0)); + } + + @Test + void testToStringIsNotNull() { + final RequestEntryInternal internal = RequestEntryInternal.builder() + .withId("id") + .withValue(ByteBuffer.wrap("x".getBytes(StandardCharsets.UTF_8))) + .withCreateTime(1L) + .build(); + + assertThat(internal.toString(), is(notNullValue())); + } + } + + @Nested + class MessageAttributesInternalTest { + + private final MessageAttributesInternal instance = MessageAttributesInternal.INSTANCE; + + @Test + void testSingletonInstanceIsNotNull() { + assertThat(instance, is(notNullValue())); + } + + @Test + void testSingletonInstanceIsSameObject() { + assertThat(MessageAttributesInternal.INSTANCE, is(instance)); + } + + @Test + void testGetEnumMessageAttributeReturnsNameLength() { + final Integer result = instance.getEnumMessageAttribute(SampleEnum.VALUE_ONE); + + assertThat(result, equalTo("VALUE_ONE".length())); + } + + @Test + void testGetEnumMessageAttributeShortName() { + final Integer result = instance.getEnumMessageAttribute(SampleEnum.A); + + assertThat(result, equalTo(1)); + } + + @Test + void testGetStringMessageAttributeReturnsLength() { + final Integer result = instance.getStringMessageAttribute("hello"); + + assertThat(result, equalTo(5)); + } + + @Test + void testGetStringMessageAttributeEmptyString() { + final Integer result = instance.getStringMessageAttribute(""); + + assertThat(result, equalTo(0)); + } + + @Test + void testGetNumberMessageAttributeInteger() { + final Integer result = instance.getNumberMessageAttribute(12345); + + assertThat(result, equalTo(5)); + } + + @Test + void testGetNumberMessageAttributeFloat() { + final Integer result = instance.getNumberMessageAttribute(3.14f); + + assertThat(result, equalTo(String.valueOf(3.14f).length())); + } + + @Test + void testGetNumberMessageAttributeNegativeNumber() { + final Integer result = instance.getNumberMessageAttribute(-99); + + assertThat(result, equalTo(3)); + } + + @Test + void testGetBinaryMessageAttributeReturnsRemaining() { + final byte[] bytes = new byte[] { 10, 20, 30 }; + final ByteBuffer buffer = ByteBuffer.wrap(bytes); + + final Integer result = instance.getBinaryMessageAttribute(buffer); + + assertThat(result, equalTo(3)); + } + + @Test + void testGetBinaryMessageAttributeEmptyBuffer() { + final ByteBuffer buffer = ByteBuffer.wrap(new byte[0]); + + final Integer result = instance.getBinaryMessageAttribute(buffer); + + assertThat(result, equalTo(0)); + } + + @Test + void testGetStringArrayMessageAttributeReturnsCombinedLength() { + final List values = Arrays.asList("a", "b", "c"); + + final Integer result = instance.getStringArrayMessageAttribute(values); + + assertThat(result, greaterThan(0)); + } + + @Test + void testGetStringArrayMessageAttributeEmptyList() { + final Integer result = instance.getStringArrayMessageAttribute(Collections.emptyList()); + + assertThat(result, greaterThanOrEqualTo(0)); + } + } + + private enum SampleEnum { + A, VALUE_ONE + } + +} \ No newline at end of file diff --git a/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/exception/MaximumAllowedMessageExceptionTest.java b/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/exception/MaximumAllowedMessageExceptionTest.java new file mode 100644 index 0000000..18cd187 --- /dev/null +++ b/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/exception/MaximumAllowedMessageExceptionTest.java @@ -0,0 +1,229 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.sns.messaging.lib.exception; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.amazon.sns.messaging.lib.model.RequestEntry; + +class MaximumAllowedMessageExceptionTest { + + private static final String ERROR_MESSAGE = "Message exceeds the maximum allowed size of 256 KB"; + + private RequestEntry buildRequestEntry() { + return RequestEntry.builder().withId("req-1").withValue("oversized payload").build(); + } + + @Nested + class HierarchyAndContract { + + @Test + void testIsInstanceOfRuntimeException() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, buildRequestEntry()); + + assertThat(ex, is(instanceOf(RuntimeException.class))); + } + + @Test + void testIsInstanceOfException() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, buildRequestEntry()); + + assertThat(ex, is(instanceOf(Exception.class))); + } + + } + + @Nested + class Constructor { + + @Test + void testConstructorSetsDetailMessage() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, buildRequestEntry()); + + assertThat(ex.getMessage(), equalTo(ERROR_MESSAGE)); + } + + @Test + void testConstructorSetsRequestEntry() { + final RequestEntry entry = buildRequestEntry(); + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, entry); + + assertThat(ex.getRequest(), is(sameInstance(entry))); + } + + @Test + void testConstructorWithNullMessageSetsNullMessage() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(null, buildRequestEntry()); + + assertThat(ex.getMessage(), is(nullValue())); + } + + @Test + void testConstructorWithNullRequestSetsNullRequest() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, null); + + assertThat(ex.getRequest(), is(nullValue())); + } + + @Test + void testConstructorWithBothNullsDoesNotThrow() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(null, null); + + assertThat(ex, is(notNullValue())); + } + + @Test + void testConstructorWithEmptyMessageSetsEmptyMessage() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException("", buildRequestEntry()); + + assertThat(ex.getMessage(), equalTo("")); + } + } + + @Nested + class GetMessage { + + @Test + void testGetMessageReturnsExactString() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, buildRequestEntry()); + + assertThat(ex.getMessage(), equalTo(ERROR_MESSAGE)); + } + + @Test + void testGetMessageWithSpecialCharactersPreservesContent() { + final String special = "Error: size=262144 > max=262144 \u00e9\u00e0"; + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(special, buildRequestEntry()); + + assertThat(ex.getMessage(), equalTo(special)); + } + + @Test + void testGetMessageIsNotNull() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, buildRequestEntry()); + + assertThat(ex.getMessage(), is(notNullValue())); + } + } + + @Nested + class GetRequest { + + @Test + void testGetRequestReturnsNotNull() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, buildRequestEntry()); + + assertThat(ex.getRequest(), is(notNullValue())); + } + + @Test + void testGetRequestReturnsSameInstancePassedToConstructor() { + final RequestEntry entry = buildRequestEntry(); + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, entry); + + assertThat(ex.getRequest(), is(sameInstance(entry))); + } + + @Test + void testGetRequestWithTypedPayloadReturnsCorrectId() { + final RequestEntry entry = buildRequestEntry(); + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, entry); + + assertThat(ex.getRequest().getId(), equalTo("req-1")); + } + + @Test + void testGetRequestWithTypedPayloadReturnsCorrectValue() { + final RequestEntry entry = buildRequestEntry(); + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, entry); + + assertThat(ex.getRequest().getValue(), equalTo("oversized payload")); + } + + @Test + void testGetRequestWithIntegerPayload() { + final RequestEntry entry = RequestEntry.builder().withId("req-int").withValue(999).build(); + + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, entry); + + assertThat(ex.getRequest().getValue(), equalTo(999)); + } + + @Test + void testGetRequestReturnsNullWhenNullPassedToConstructor() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, null); + + assertThat(ex.getRequest(), is(nullValue())); + } + } + + @Nested + class Throwability { + + @Test + void testExceptionCanBeThrown() { + final RequestEntry entry = buildRequestEntry(); + + try { + throw new MaximumAllowedMessageException(ERROR_MESSAGE, entry); + } catch (final MaximumAllowedMessageException ex) { + assertThat(ex.getMessage(), equalTo(ERROR_MESSAGE)); + assertThat(ex.getRequest(), is(sameInstance(entry))); + } + } + + @Test + void testExceptionCanBeCaughtAsRuntimeException() { + final RequestEntry entry = buildRequestEntry(); + + try { + throw new MaximumAllowedMessageException(ERROR_MESSAGE, entry); + } catch (final RuntimeException ex) { + assertThat(ex, is(instanceOf(MaximumAllowedMessageException.class))); + } + } + + @Test + void testExceptionPreservesStackTrace() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, buildRequestEntry()); + + assertThat(ex.getStackTrace(), is(notNullValue())); + assertThat(ex.getStackTrace().length, is(greaterThan(0))); + } + + @Test + void testExceptionCauseIsNullByDefault() { + final MaximumAllowedMessageException ex = new MaximumAllowedMessageException(ERROR_MESSAGE, buildRequestEntry()); + + assertThat(ex.getCause(), is(nullValue())); + } + } + + private static org.hamcrest.Matcher greaterThan(final int value) { + return org.hamcrest.Matchers.greaterThan(value); + } + +} \ No newline at end of file diff --git a/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/metrics/AbstractAmazonSnsConsumerMetricsDecoratorTest.java b/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/metrics/AbstractAmazonSnsConsumerMetricsDecoratorTest.java new file mode 100644 index 0000000..f2ae77b --- /dev/null +++ b/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/metrics/AbstractAmazonSnsConsumerMetricsDecoratorTest.java @@ -0,0 +1,441 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.sns.messaging.lib.metrics; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.amazon.sns.messaging.lib.core.AmazonSnsConsumer; +import com.amazon.sns.messaging.lib.model.TopicProperty; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +@ExtendWith(MockitoExtension.class) +class AbstractAmazonSnsConsumerMetricsDecoratorTest { + + private static class TestableDecorator extends AbstractAmazonSnsConsumerMetricsDecorator { + + TestableDecorator(final AmazonSnsConsumer delegate, final TopicProperty topicProperty, final MeterRegistry meterRegistry) { + super(delegate, topicProperty, meterRegistry); + } + + @Override + public void handleError(final Object publishBatchRequest, final Throwable throwable) { + // no-op for testing + } + + @Override + public void handleResponse(final Object publishBatchResult) { + // no-op for testing + } + + @Override + public Object publish(final Object publishBatchRequest) { + return null; + } + + } + + private static final String TOPIC_ARN = "arn:aws:sns:us-east-1:000000000000:my-topic"; + + private TestableDecorator decorator; + + @Mock + private AmazonSnsConsumer delegate; + + @Mock + private TopicProperty topicProperty; + + @Spy + private SimpleMeterRegistry meterRegistry; + + @BeforeEach + void setup() { + when(topicProperty.getTopicArn()).thenReturn(TOPIC_ARN); + + decorator = new TestableDecorator(delegate, topicProperty, meterRegistry); + } + + @Nested + class MetricNameConstants { + + @Test + void testMetricPublishAttemptsHasSnsPrefix() { + assertThat(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_ATTEMPTS, containsString("sns")); + } + + @Test + void testMetricPublishAttemptsValue() { + assertThat(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_ATTEMPTS, equalTo("sns.publish.attempts")); + } + + @Test + void testMetricPublishSuccessValue() { + assertThat(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_SUCCESS, equalTo("sns.publish.success")); + } + + @Test + void testMetricPublishFailureValue() { + assertThat(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_FAILURE, equalTo("sns.publish.failure")); + } + + @Test + void testMetricPublishDurationValue() { + assertThat(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_DURATION, equalTo("sns.publish.duration")); + } + + @Test + void testMetricPublishBatchSizeValue() { + assertThat(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_BATCH_SIZE, equalTo("sns.publish.batch.size")); + } + + @Test + void testMetricPublishInflightValue() { + assertThat(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_INFLIGHT, equalTo("sns.publish.inflight")); + } + + @Test + void testTagErrorCodeValue() { + assertThat(AbstractAmazonSnsConsumerMetricsDecorator.TAG_ERROR_CODE, equalTo("error_code")); + } + + @Test + void testTagErrorTypeValue() { + assertThat(AbstractAmazonSnsConsumerMetricsDecorator.TAG_ERROR_TYPE, equalTo("error_type")); + } + + @Test + void testErrorTypeAmazonValue() { + assertThat(AbstractAmazonSnsConsumerMetricsDecorator.ERROR_TYPE_AMAZON, equalTo("amazon_service_exception")); + } + + @Test + void testErrorTypeOtherValue() { + assertThat(AbstractAmazonSnsConsumerMetricsDecorator.ERROR_TYPE_OTHER, equalTo("unknown")); + } + } + + @Nested + class ConstructorInitialization { + + @Test + void testDelegateIsSet() { + assertThat(decorator.delegate, is(sameInstance(delegate))); + } + + @Test + void testRegistryIsNotNull() { + assertThat(decorator.registry, is(notNullValue())); + } + + @Test + void testTagsAreNotNull() { + assertThat(decorator.tags, is(notNullValue())); + } + + @Test + void testTagsContainTopicArn() { + assertThat(decorator.tags.stream().anyMatch(t -> "topic".equals(t.getKey()) && TOPIC_ARN.equals(t.getValue())), is(true)); + } + + @Test + void testPublishAttemptsCounterIsNotNull() { + assertThat(decorator.publishAttemptsCounter, is(notNullValue())); + } + + @Test + void testSuccessCounterIsNotNull() { + assertThat(decorator.successCounter, is(notNullValue())); + } + + @Test + void testPublishTimerIsNotNull() { + assertThat(decorator.publishTimer, is(notNullValue())); + } + + @Test + void testBatchSizeSummaryIsNotNull() { + assertThat(decorator.batchSizeSummary, is(notNullValue())); + } + + @Test + void testInflightGaugeIsNotNull() { + assertThat(decorator.inflightGauge, is(notNullValue())); + } + + @Test + void testInflightGaugeInitialValueIsZero() { + assertThat(decorator.inflightGauge.get(), equalTo(0)); + } + + @Test + void testConstructorWithNullMeterRegistryDoesNotThrow() { + final TestableDecorator nullRegistryDecorator = new TestableDecorator(delegate, topicProperty, null); + + assertThat(nullRegistryDecorator, is(notNullValue())); + } + + @Test + void testConstructorWithNullMeterRegistryInitializesCounters() { + final TestableDecorator nullRegistryDecorator = new TestableDecorator(delegate, topicProperty, null); + + assertThat(nullRegistryDecorator.publishAttemptsCounter, is(notNullValue())); + assertThat(nullRegistryDecorator.successCounter, is(notNullValue())); + } + } + + @Nested + class MetersRegisteredInRegistry { + + @Test + void testPublishAttemptsCounterRegisteredInRegistry() { + assertThat(meterRegistry.find(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_ATTEMPTS).counter(), is(notNullValue())); + } + + @Test + void testPublishSuccessCounterRegisteredInRegistry() { + assertThat(meterRegistry.find(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_SUCCESS).counter(), is(notNullValue())); + } + + @Test + void testPublishTimerRegisteredInRegistry() { + assertThat(meterRegistry.find(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_DURATION).timer(), is(notNullValue())); + } + + @Test + void testBatchSizeSummaryRegisteredInRegistry() { + assertThat(meterRegistry.find(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_BATCH_SIZE).summary(), is(notNullValue())); + } + + @Test + void testInflightGaugeRegisteredInRegistry() { + assertThat(meterRegistry.find(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_INFLIGHT).gauge(), is(notNullValue())); + } + + @Test + void testPublishAttemptsCounterInitialValueIsZero() { + assertThat(decorator.publishAttemptsCounter.count(), equalTo(0.0)); + } + + @Test + void testSuccessCounterInitialValueIsZero() { + assertThat(decorator.successCounter.count(), equalTo(0.0)); + } + + @Test + void testPublishAttemptsCounterIncrementsCorrectly() { + decorator.publishAttemptsCounter.increment(); + + assertThat(decorator.publishAttemptsCounter.count(), equalTo(1.0)); + } + + @Test + void testSuccessCounterIncrementsCorrectly() { + decorator.successCounter.increment(3); + + assertThat(decorator.successCounter.count(), equalTo(3.0)); + } + } + + @Nested + class FailureCounter { + + @Test + void testFailureCounterReturnsNotNull() { + final Counter counter = decorator.failureCounter("400", "amazon_service_exception"); + + assertThat(counter, is(notNullValue())); + } + + @Test + void testFailureCounterInitialValueIsZero() { + final Counter counter = decorator.failureCounter("500", "unknown"); + + assertThat(counter.count(), equalTo(0.0)); + } + + @Test + void testFailureCounterIncrementsCorrectly() { + final Counter counter = decorator.failureCounter("400", "amazon_service_exception"); + counter.increment(); + + assertThat(counter.count(), equalTo(1.0)); + } + + @Test + void testFailureCounterRegisteredInRegistry() { + decorator.failureCounter("InvalidParameter", "amazon_service_exception"); + + assertThat(meterRegistry.find(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_FAILURE).counter(), is(notNullValue())); + } + + @Test + void testFailureCounterWithSameTagsReturnsSameMeter() { + final Counter first = decorator.failureCounter("400", "amazon_service_exception"); + final Counter second = decorator.failureCounter("400", "amazon_service_exception"); + + assertThat(first, is(sameInstance(second))); + } + + @Test + void testFailureCounterWithDifferentErrorCodesAreDistinct() { + final Counter counter400 = decorator.failureCounter("400", "amazon_service_exception"); + final Counter counter500 = decorator.failureCounter("500", "amazon_service_exception"); + + assertThat(counter400, is(not(sameInstance(counter500)))); + } + + @Test + void testFailureCounterWithDifferentErrorTypesAreDistinct() { + final Counter amazon = decorator.failureCounter("400", "amazon_service_exception"); + final Counter unknown = decorator.failureCounter("400", "unknown"); + + assertThat(amazon, is(not(sameInstance(unknown)))); + } + + @Test + void testFailureCounterTagsIncludeTopicArn() { + decorator.failureCounter("400", "amazon_service_exception"); + + final Counter found = meterRegistry.find(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_FAILURE).tag("topic", TOPIC_ARN).counter(); + + assertThat(found, is(notNullValue())); + } + + @Test + void testFailureCounterTagsIncludeErrorCode() { + decorator.failureCounter("InvalidParam", "amazon_service_exception"); + + final Counter found = meterRegistry.find(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_FAILURE).tag(AbstractAmazonSnsConsumerMetricsDecorator.TAG_ERROR_CODE, "InvalidParam").counter(); + + assertThat(found, is(notNullValue())); + } + + @Test + void testFailureCounterTagsIncludeErrorType() { + decorator.failureCounter("400", "unknown"); + + final Counter found = meterRegistry.find(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_FAILURE).tag(AbstractAmazonSnsConsumerMetricsDecorator.TAG_ERROR_TYPE, "unknown").counter(); + + assertThat(found, is(notNullValue())); + } + } + + @Nested + class InflightGauge { + + @Test + void testInflightGaugeReflectsIncrementInRegistry() { + decorator.inflightGauge.incrementAndGet(); + + final double gaugeValue = meterRegistry.find(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_INFLIGHT).gauge().value(); + + assertThat(gaugeValue, equalTo(1.0)); + } + + @Test + void testInflightGaugeReflectsDecrementInRegistry() { + decorator.inflightGauge.set(3); + decorator.inflightGauge.decrementAndGet(); + + final double gaugeValue = meterRegistry.find(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_INFLIGHT).gauge().value(); + + assertThat(gaugeValue, equalTo(2.0)); + } + + @Test + void testInflightGaugeReflectsZeroAfterReset() { + decorator.inflightGauge.set(5); + decorator.inflightGauge.set(0); + + final double gaugeValue = meterRegistry.find(AbstractAmazonSnsConsumerMetricsDecorator.METRIC_PUBLISH_INFLIGHT).gauge().value(); + + assertThat(gaugeValue, equalTo(0.0)); + } + } + + @Nested + class Shutdown { + + @Test + void testShutdownDelegatesToDelegate() { + decorator.shutdown(); + + verify(delegate).shutdown(); + } + + @Test + void testShutdownCanBeCalledMultipleTimes() { + decorator.shutdown(); + decorator.shutdown(); + + verify(delegate, org.mockito.Mockito.times(2)).shutdown(); + } + } + + @Nested + class Await { + + @Test + void testAwaitDelegatesToDelegate() { + final CompletableFuture future = CompletableFuture.completedFuture(null); + when(delegate.await()).thenReturn(future); + + final CompletableFuture result = decorator.await(); + + assertThat(result, is(sameInstance(future))); + verify(delegate).await(); + } + + @Test + void testAwaitReturnsNotNull() { + when(delegate.await()).thenReturn(CompletableFuture.completedFuture(null)); + + assertThat(decorator.await(), is(notNullValue())); + } + + @Test + void testAwaitPropagatesDelegateResult() { + final CompletableFuture expected = new CompletableFuture<>(); + when(delegate.await()).thenReturn(expected); + + final CompletableFuture result = decorator.await(); + + assertThat(result, is(sameInstance(expected))); + } + } + +} \ No newline at end of file