diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-900697b.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-900697b.json
new file mode 100644
index 000000000000..d2ceb2c8cbeb
--- /dev/null
+++ b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-900697b.json
@@ -0,0 +1,6 @@
+{
+ "type": "feature",
+ "category": "Amazon DynamoDB Enhanced Client",
+ "contributor": "",
+ "description": "Increase code coverage on dynamodb-enhanced module"
+}
diff --git a/services-custom/dynamodb-enhanced/pom.xml b/services-custom/dynamodb-enhanced/pom.xml
index 6fbb019d8414..d207457959f0 100644
--- a/services-custom/dynamodb-enhanced/pom.xml
+++ b/services-custom/dynamodb-enhanced/pom.xml
@@ -208,6 +208,11 @@
DynamoDBLocal
test
+
+ org.slf4j
+ slf4j-api
+ test
+
com.almworks.sqlite4java
sqlite4java
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProviderTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProviderTest.java
new file mode 100644
index 000000000000..417c8fb24547
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProviderTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.enhanced.dynamodb;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.List;
+import org.apache.logging.log4j.core.LogEvent;
+import org.junit.jupiter.api.Test;
+import org.slf4j.event.Level;
+
+public class DefaultAttributeConverterProviderTest {
+
+ @Test
+ void findConverter_whenConverterFound_logsConverterFound() {
+ try (LogCaptor logCaptor = new LogCaptor(DefaultAttributeConverterProvider.class, Level.DEBUG)) {
+ DefaultAttributeConverterProvider provider = DefaultAttributeConverterProvider.create();
+ provider.converterFor(EnhancedType.of(String.class));
+
+ List logEvents = logCaptor.loggedEvents();
+ assertThat(logEvents).hasSize(1);
+ assertThat(logEvents.get(0).getLevel().name()).isEqualTo(Level.DEBUG.name());
+ assertThat(logEvents.get(0).getMessage().getFormattedMessage())
+ .contains("Converter for EnhancedType(java.lang.String): software.amazon.awssdk.enhanced.dynamodb.internal"
+ + ".converter.attribute.StringAttributeConverter");
+ }
+ }
+
+ @Test
+ void findConverter_whenConverterNotFound_logsNoConverter() {
+ try (LogCaptor logCaptor = new LogCaptor(DefaultAttributeConverterProvider.class, Level.DEBUG)) {
+ DefaultAttributeConverterProvider provider = DefaultAttributeConverterProvider.create();
+
+ assertThatThrownBy(() -> provider.converterFor(EnhancedType.of(CustomUnsupportedType.class)))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Converter not found for EnhancedType(software.amazon.awssdk.enhanced.dynamodb"
+ + ".DefaultAttributeConverterProviderTest$CustomUnsupportedType)");
+ List logEvents = logCaptor.loggedEvents();
+ assertThat(logEvents).hasSize(1);
+ assertThat(logEvents.get(0).getLevel().name()).isEqualTo(Level.DEBUG.name());
+ assertThat(logEvents.get(0).getMessage().getFormattedMessage())
+ .contains("No converter available for EnhancedType(software.amazon.awssdk.enhanced.dynamodb"
+ + ".DefaultAttributeConverterProviderTest$CustomUnsupportedType)");
+ }
+ }
+
+ /**
+ * A custom type with no converter registered for it.
+ */
+ private static class CustomUnsupportedType {
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/DefaultMethodsUnsupportedOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/DefaultMethodsUnsupportedOperationTest.java
new file mode 100644
index 000000000000..2e04f9bd1871
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/DefaultMethodsUnsupportedOperationTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.enhanced.dynamodb;
+
+import static java.util.stream.Collectors.toList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+
+/**
+ * Test class that discovers all interfaces with default methods that throw UnsupportedOperationException. Shows individual test
+ * scenarios and results using DynamicTest.
+ */
+public class DefaultMethodsUnsupportedOperationTest {
+
+ private static final String BASE_PACKAGE = "software.amazon.awssdk.enhanced.dynamodb";
+ private static final Pattern CLASS_PATTERN = Pattern.compile(".class", Pattern.LITERAL);
+
+ private static final List testScenarios = Collections.synchronizedList(new java.util.ArrayList<>());
+
+ @TestFactory
+ Stream testDefaultMethodsThrowUnsupportedOperation() {
+ List dynamicTestList = scanPackageForClasses(BASE_PACKAGE)
+ .filter(Class::isInterface)
+ .filter(this::hasDefaultMethods)
+ .collect(toList())
+ .stream()
+ .flatMap(this::createTestsForInterface)
+ .collect(toList());
+ assertEquals(102, dynamicTestList.size());
+ return dynamicTestList.stream();
+ }
+
+ private Stream> scanPackageForClasses(String packageName) {
+ try {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ return Collections.list(loader.getResources(packageName.replace('.', '/')))
+ .stream()
+ .map(URL::getFile)
+ .map(File::new)
+ .filter(File::exists)
+ .flatMap(dir -> findClassesInDirectory(dir, packageName));
+ } catch (Exception e) {
+ return Stream.empty();
+ }
+ }
+
+ private Stream> findClassesInDirectory(File dir, String packageName) {
+ return Optional.ofNullable(dir.listFiles())
+ .map(Arrays::stream)
+ .orElseGet(Stream::empty)
+ .flatMap(file ->
+ file.isDirectory()
+ ? findClassesInDirectory(file, packageName + "." + file.getName())
+ : loadClassFromFile(file, packageName));
+ }
+
+ private Stream> loadClassFromFile(File file, String packageName) {
+ if (!file.getName().endsWith(".class")) {
+ return Stream.empty();
+ }
+
+ String className = packageName + '.' + CLASS_PATTERN.matcher(file.getName()).replaceAll("");
+ try {
+ return Stream.of(Class.forName(className));
+ } catch (ClassNotFoundException | NoClassDefFoundError e) {
+ return Stream.empty();
+ }
+ }
+
+ private boolean hasDefaultMethods(Class> interfaceClass) {
+ return Arrays.stream(interfaceClass.getDeclaredMethods())
+ .anyMatch(Method::isDefault);
+ }
+
+ private Stream createTestsForInterface(Class> interfaceClass) {
+ return Arrays.stream(interfaceClass.getDeclaredMethods())
+ .filter(Method::isDefault)
+ .filter(method -> throwsUnsupportedOperation(interfaceClass, method))
+ .map(method -> {
+ String testName = String.format("%s.%s() → throws UnsupportedOperationException",
+ interfaceClass.getSimpleName(),
+ method.getName());
+ testScenarios.add(testName);
+
+ return DynamicTest.dynamicTest(testName, () ->
+ testMethodThrowsUnsupportedOperation(interfaceClass, method));
+ });
+ }
+
+ private boolean throwsUnsupportedOperation(Class> interfaceClass, Method method) {
+ try {
+ Object mockInstance = createMockInstance(interfaceClass);
+ Object[] args = createArguments(method);
+ method.invoke(mockInstance, args);
+ return false;
+ } catch (Exception e) {
+ Throwable cause = e.getCause() != null ? e.getCause() : e;
+ return cause instanceof UnsupportedOperationException;
+ }
+ }
+
+ private void testMethodThrowsUnsupportedOperation(Class> interfaceClass, Method method) {
+ Object mockInstance = createMockInstance(interfaceClass);
+ Object[] args = createArguments(method);
+
+ assertThrows(UnsupportedOperationException.class, () -> {
+ try {
+ method.invoke(mockInstance, args);
+ } catch (Exception e) {
+ Throwable cause = e.getCause() != null ? e.getCause() : e;
+ if (cause instanceof UnsupportedOperationException) {
+ throw cause;
+ }
+ throw new RuntimeException(cause);
+ }
+ }, () -> String.format("Expected %s.%s() to throw UnsupportedOperationException",
+ interfaceClass.getSimpleName(), method.getName()));
+ }
+
+ private T createMockInstance(Class interfaceClass) {
+ T mock = mock(interfaceClass, CALLS_REAL_METHODS);
+ if (mock instanceof MappedTableResource) {
+ when(((MappedTableResource>) mock).tableName()).thenReturn("test-table");
+ }
+ return mock;
+ }
+
+ private Object[] createArguments(Method method) {
+ return Arrays.stream(method.getParameterTypes()).map(this::createArgument).toArray();
+ }
+
+ private Object createArgument(Class> paramType) {
+ if (paramType == String.class) {
+ return "test";
+ }
+ if (paramType == Key.class) {
+ return Key.builder().partitionValue("test").build();
+ }
+ if (Consumer.class.isAssignableFrom(paramType)) {
+ return (Consumer>) obj -> {
+ };
+ }
+ if (paramType.isInterface()) {
+ return mock(paramType);
+ }
+ try {
+ return mock(paramType);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EqualsAndHashCodeTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EqualsAndHashCodeTest.java
new file mode 100644
index 000000000000..a2cb9e493a11
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EqualsAndHashCodeTest.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.enhanced.dynamodb;
+
+import static java.lang.reflect.Modifier.isAbstract;
+import static java.lang.reflect.Modifier.isInterface;
+import static java.util.stream.Collectors.toList;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import nl.jqno.equalsverifier.EqualsVerifier;
+import nl.jqno.equalsverifier.Warning;
+import nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+
+/**
+ * Test class for testing equals/hashCode methods for all enhanced DynamoDB classes in the main source set.
+ */
+public class EqualsAndHashCodeTest {
+
+ private static final String ROOT_PACKAGE = "software.amazon.awssdk.enhanced.dynamodb";
+ private static final String ROOT_PATH = ROOT_PACKAGE.replace('.', '/');
+ private static final Pattern CLASS_PATTERN = Pattern.compile(".class", Pattern.LITERAL);
+
+
+ @TestFactory
+ Stream verifyEqualsAndHashCodeForAllMainClasses() throws Exception {
+ List> testableClasses = findAllClassesUnderRootPackage()
+ .stream()
+ .filter(type -> isConcreteClass(type) && overridesEqualsOrHashCode(type))
+ .collect(toList());
+
+ return testableClasses.stream()
+ .map(this::createEqualsHashCodeTest)
+ .collect(toList())
+ .stream();
+ }
+
+ private DynamicTest createEqualsHashCodeTest(Class> type) {
+ String testName = "equals/hashCode: " + type.getSimpleName();
+ return DynamicTest.dynamicTest(testName, () -> verifyEqualsAndHashCode(type));
+ }
+
+ private List> findAllClassesUnderRootPackage() throws Exception {
+ List> classes = new ArrayList<>();
+
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ Enumeration resources = classLoader.getResources(ROOT_PATH);
+
+ while (resources.hasMoreElements()) {
+ URL resource = resources.nextElement();
+ if (!"file".equals(resource.getProtocol())) {
+ continue;
+ }
+
+ URI uri = resource.toURI();
+ File directory = new File(uri);
+ scanDirectory(directory, ROOT_PACKAGE, classes);
+ }
+
+ return classes;
+ }
+
+ private void scanDirectory(File dir, String pkg, List> classes) throws ClassNotFoundException {
+ File[] files = dir.listFiles();
+ if (files == null) {
+ return;
+ }
+
+ for (File file : files) {
+ if (file.isDirectory()) {
+ scanDirectory(file, pkg + "." + file.getName(), classes);
+ } else if (isMainClass(file)) {
+ classes.add(Class.forName(pkg + '.' + CLASS_PATTERN.matcher(file.getName()).replaceAll("")));
+ }
+ }
+ }
+
+ private boolean isMainClass(File file) {
+ return file.getName().endsWith(".class")
+ && (file.getPath().contains("target/classes")
+ || file.getPath().contains("build/classes/java/main"));
+ }
+
+ private boolean isConcreteClass(Class> type) {
+ int m = type.getModifiers();
+ return !isAbstract(m)
+ && !isInterface(m)
+ && !type.isEnum()
+ && !type.isAnonymousClass()
+ && !type.isLocalClass();
+ }
+
+ private boolean overridesEqualsOrHashCode(Class> type) {
+ try {
+ return (type.getDeclaredMethod("equals", Object.class).getDeclaringClass() != Object.class)
+ || (type.getDeclaredMethod("hashCode").getDeclaringClass() != Object.class);
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ private void verifyEqualsAndHashCode(Class> type) {
+ SingleTypeEqualsVerifierApi> verifier =
+ EqualsVerifier.forClass(type)
+ .withPrefabValues(
+ EnhancedType.class,
+ EnhancedType.of(String.class),
+ EnhancedType.of(Integer.class))
+ .withPrefabValues(
+ AttributeValue.class,
+ AttributeValue.builder().s("one").build(),
+ AttributeValue.builder().s("two").build());
+
+ String className = type.getName();
+
+ switch (className) {
+ case "software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue": {
+ verifier = verifier.withNonnullFields("type");
+ break;
+ }
+ case "software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticIndexMetadata": {
+ verifier = verifier.withNonnullFields("partitionKeys", "sortKeys")
+ .usingGetClass();
+ break;
+ }
+ case "software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticKeyAttributeMetadata": {
+ verifier = verifier.withNonnullFields("order")
+ .usingGetClass();
+ break;
+ }
+ case "software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument": {
+ // Provide non-equal prefab values for nonAttributeValueMap to avoid NullPointerException and Precondition error
+ Map map1 = new HashMap<>();
+ Map map2 = new HashMap<>();
+ map2.put("key", "value");
+ verifier = verifier.withPrefabValues(
+ Map.class,
+ map1,
+ map2)
+ .suppress(Warning.STRICT_HASHCODE)
+ .withNonnullFields("nonAttributeValueMap",
+ "attributeValueMap",
+ "attributeConverterProviders",
+ "attributeConverterChain")
+ .usingGetClass();
+ break;
+ }
+ case "software.amazon.awssdk.enhanced.dynamodb.EnhancedType": {
+ // Suppress warning about subclass equality
+ verifier = verifier.suppress(nl.jqno.equalsverifier.Warning.STRICT_INHERITANCE)
+ .withNonnullFields("rawClass")
+ .usingGetClass();
+ break;
+ }
+ case "software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata" : {
+ verifier = verifier.withIgnoredFields("partitionKeyCache", "sortKeyCache");
+ break;
+ }
+ case "software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest": {
+ verifier = verifier.withIgnoredFields("ignoreNullsMode");
+ break;
+ }
+ case "software.amazon.awssdk.enhanced.dynamodb.model.TransactUpdateItemEnhancedRequest": {
+ verifier = verifier.withIgnoredFields("ignoreNullsMode").usingGetClass();
+ break;
+ }
+ default: {
+ if (Arrays.asList(
+ "software.amazon.awssdk.enhanced.dynamodb.internal.conditional.EqualToConditional",
+ "software.amazon.awssdk.enhanced.dynamodb.internal.conditional.SingleKeyItemConditional",
+ "software.amazon.awssdk.enhanced.dynamodb.internal.conditional.BetweenConditional",
+ "software.amazon.awssdk.enhanced.dynamodb.internal.conditional.BeginsWithConditional",
+ "software.amazon.awssdk.enhanced.dynamodb.internal.mapper.AtomicCounter$CounterAttribute",
+ "software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticKeyAttributeMetadata",
+ "software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext",
+ "software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbIndex",
+ "software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbTable",
+ "software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticIndexMetadata")
+ .contains(className)) {
+ verifier = verifier.usingGetClass();
+ }
+ break;
+ }
+ }
+
+ verifier.verify();
+ }
+}
\ No newline at end of file
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/LogCaptor.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/LogCaptor.java
new file mode 100644
index 000000000000..d12dbf63ddda
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/LogCaptor.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.enhanced.dynamodb;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.LoggerConfig;
+
+public final class LogCaptor implements AutoCloseable {
+ private final LoggerContext loggerContext;
+ private final Configuration config;
+
+ private final String loggerName;
+ private final String appenderName;
+
+ private final LoggerConfig initialLoggerConfig;
+ private final Level initialLoggerLevel;
+ private final LoggerConfig dedicatedLoggerConfig;
+
+ private final TestAppender testAppender;
+
+ public LogCaptor(Class> loggerClass, org.slf4j.event.Level level) {
+ this(loggerClass.getName(), level);
+ }
+
+ public LogCaptor(String loggerName, org.slf4j.event.Level level) {
+ this.loggerName = loggerName;
+ this.appenderName = "TestAppender#" + loggerName;
+ Level levelToCapture = Level.valueOf(level.name());
+
+ this.loggerContext = (LoggerContext) LogManager.getContext(false);
+ this.config = loggerContext.getConfiguration();
+
+ this.testAppender = new TestAppender(appenderName);
+ this.testAppender.start();
+
+ this.config.addAppender(this.testAppender);
+
+ LoggerConfig existingLoggerConfig = config.getLoggerConfig(loggerName);
+
+ if (!existingLoggerConfig.getName().equals(loggerName)) {
+ LoggerConfig dedicatedLoggerConfig = new LoggerConfig(loggerName, levelToCapture, false);
+ dedicatedLoggerConfig.addAppender(this.testAppender, levelToCapture, null);
+ this.config.addLogger(loggerName, dedicatedLoggerConfig);
+ this.initialLoggerLevel = null;
+ this.dedicatedLoggerConfig = dedicatedLoggerConfig;
+ this.initialLoggerConfig = dedicatedLoggerConfig;
+ } else {
+ existingLoggerConfig.addAppender(this.testAppender, levelToCapture, null);
+ this.initialLoggerLevel = existingLoggerConfig.getLevel();
+ existingLoggerConfig.setLevel(levelToCapture);
+ this.dedicatedLoggerConfig = null;
+ this.initialLoggerConfig = existingLoggerConfig;
+ }
+
+ this.loggerContext.updateLoggers();
+ }
+
+ public List loggedEvents() {
+ return this.testAppender.getEvents();
+ }
+
+ @Override
+ public void close() {
+ this.initialLoggerConfig.removeAppender(appenderName);
+
+ if (this.dedicatedLoggerConfig != null) {
+ this.config.removeLogger(loggerName);
+ } else if (this.initialLoggerLevel != null) {
+ this.initialLoggerConfig.setLevel(this.initialLoggerLevel);
+ }
+
+ this.config.getAppenders().remove(appenderName);
+ this.testAppender.stop();
+
+ this.loggerContext.updateLoggers();
+ }
+
+ private static final class TestAppender extends AbstractAppender {
+
+ private final List events = new ArrayList<>();
+
+ private TestAppender(String appenderName) {
+ super(appenderName, null, null, true, null);
+ }
+
+ @Override
+ public void append(LogEvent event) {
+ this.events.add(event.toImmutable());
+ }
+
+ public List getEvents() {
+ return Collections.unmodifiableList(this.events);
+ }
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadataCompositeKeyTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadataCompositeKeyTest.java
index 5afe4e9b52c2..b1b5aba53246 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadataCompositeKeyTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadataCompositeKeyTest.java
@@ -77,4 +77,14 @@ void backwardCompatibility_newMethodsMatchDeprecated() {
assertThat(newPartitionKeys).hasSize(1);
assertThat(newPartitionKeys.get(0)).isEqualTo(deprecatedPartitionKey);
}
+
+ @Test
+ void indexPartitionKeys_withValidIndex_returnsSingletonList() {
+ TableMetadata metadata = INDEXED_SCHEMA.tableMetadata();
+
+ List result = metadata.indexPartitionKeys("gsi_1");
+
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0)).isEqualTo("gsi_id");
+ }
}
\ No newline at end of file
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaTest.java
index 198b4a653577..961c08efc120 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaTest.java
@@ -17,16 +17,22 @@
import static org.assertj.core.api.Assertions.assertThat;
+import java.lang.invoke.MethodHandles;
+import java.util.Collection;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem;
import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchemaParams;
import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchemaParams;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CommonTypesBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CompositeMetadataBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CrossIndexBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.DuplicateOrderBean;
@@ -39,6 +45,8 @@
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SingleKeyBean;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+import software.amazon.awssdk.utils.ImmutableMap;
public class TableSchemaTest {
@Rule
@@ -76,6 +84,17 @@ public void fromBean_constructsBeanTableSchema() {
assertThat(beanBeanTableSchema).isNotNull();
}
+ @Test
+ public void fromBean_withParams_constructsBeanTableSchema() {
+ BeanTableSchemaParams params = BeanTableSchemaParams.builder(SimpleBean.class)
+ .lookup(MethodHandles.lookup())
+ .build();
+ BeanTableSchema beanTableSchema = TableSchema.fromBean(params);
+
+ assertThat(beanTableSchema).isNotNull();
+ assertThat(beanTableSchema.itemType().rawClass()).isEqualTo(SimpleBean.class);
+ }
+
@Test
public void fromImmutable_constructsImmutableTableSchema() {
ImmutableTableSchema immutableTableSchema =
@@ -84,6 +103,17 @@ public void fromImmutable_constructsImmutableTableSchema() {
assertThat(immutableTableSchema).isNotNull();
}
+ @Test
+ public void fromImmutable_withParams_constructsImmutableTableSchema() {
+ ImmutableTableSchemaParams params = ImmutableTableSchemaParams.builder(SimpleImmutable.class)
+ .lookup(MethodHandles.lookup())
+ .build();
+ ImmutableTableSchema immutableTableSchema = TableSchema.fromImmutableClass(params);
+
+ assertThat(immutableTableSchema).isNotNull();
+ assertThat(immutableTableSchema.itemType().rawClass()).isEqualTo(SimpleImmutable.class);
+ }
+
@Test
public void fromClass_constructsBeanTableSchema() {
TableSchema tableSchema = TableSchema.fromClass(SimpleBean.class);
@@ -204,6 +234,59 @@ public void fromBean_constructsTableMetadata_withMultipleGSI_differentCompositeS
assertThat(gsi3SortKeys.size()).isEqualTo(0);
}
+ @Test
+ public void mapToItem_whenPreserveEmptyObjectTrue_throwsUnsupportedOperationException() {
+ exception.expect(UnsupportedOperationException.class);
+ exception.expectMessage("preserveEmptyObject is not supported. You can set preserveEmptyObject to "
+ + "false to continue to call this operation. If you wish to enable "
+ + "preserveEmptyObject, please reach out to the maintainers of the "
+ + "implementation class for assistance.");
+
+ TableSchema schema = new TableSchema() {
+ @Override
+ public CommonTypesBean mapToItem(Map attributeMap) {
+ return null;
+ }
+
+ @Override
+ public Map itemToMap(CommonTypesBean item, boolean ignoreNulls) {
+ return null;
+ }
+
+ @Override
+ public Map itemToMap(CommonTypesBean item, Collection attributes) {
+ return null;
+ }
+
+ @Override
+ public AttributeValue attributeValue(CommonTypesBean item, String attributeName) {
+ return null;
+ }
+
+ @Override
+ public TableMetadata tableMetadata() {
+ return null;
+ }
+
+ @Override
+ public EnhancedType itemType() {
+ return null;
+ }
+
+ @Override
+ public List attributeNames() {
+ return null;
+ }
+
+ @Override
+ public boolean isAbstract() {
+ return false;
+ }
+ };
+
+ schema.mapToItem(ImmutableMap.of("abc", AttributeValue.builder().build()), true);
+ }
+
@Test
public void fromClass_invalidClassThrowsException() {
exception.expect(IllegalArgumentException.class);
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/EnumAttributeConverterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/EnumAttributeConverterTest.java
index fe17f3050533..f4edc9e08d7c 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/EnumAttributeConverterTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/EnumAttributeConverterTest.java
@@ -20,6 +20,7 @@
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class EnumAttributeConverterTest {
@@ -74,8 +75,17 @@ public void transformToWithNames_returnsEnum() {
Person john = personConverter.transformTo(AttributeValue.fromS("JOHN"));
assertThat(Person.JOHN.toString()).isEqualTo("I am a cool person");
-
assertThat(john).isEqualTo(Person.JOHN);
+ }
+
+ @Test
+ public void transformTo_whenInputStringIsNull_throwsIllegalArgumentException() {
+ EnumAttributeConverter vehicleConverter = EnumAttributeConverter.create(Vehicle.class);
+ AttributeValue input = AttributeValue.builder().build();
+
+ assertThatThrownBy(() -> vehicleConverter.transformTo(input))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Cannot convert non-string value to enum.");
}
private static enum Vehicle {
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java
index deaf64fd962e..219043e3ce43 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DefaultEnhancedDocumentTest.java
@@ -16,13 +16,22 @@
package software.amazon.awssdk.enhanced.dynamodb.document;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider;
import static software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData.defaultDocBuilder;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.core.SdkBytes;
+import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
import software.amazon.awssdk.enhanced.dynamodb.internal.document.DefaultEnhancedDocument;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
-
class DefaultEnhancedDocumentTest {
@Test
@@ -63,4 +72,246 @@ void isNull_when_putObjectWithNullAttribute() {
DefaultEnhancedDocument document = (DefaultEnhancedDocument) builder.build();
assertThat(document.isNull("nullAttribute")).isTrue();
}
+
+ @Test
+ void getListOfUnknownType_forUnknownAttributeName_returnsNull() {
+ DefaultEnhancedDocument document = (DefaultEnhancedDocument) defaultDocBuilder()
+ .attributeConverterProviders(defaultProvider())
+ .putNull("nullAttributeName")
+ .putString("attributeName", "attributeValue")
+ .build();
+
+ List result = document.getListOfUnknownType("unknownAttributeName");
+ assertThat(result).isNull();
+ }
+
+ @Test
+ void getListOfUnknownType_forListAttributeName_returnsCorrectValue() {
+ DefaultEnhancedDocument document = (DefaultEnhancedDocument) defaultDocBuilder()
+ .attributeConverterProviders(defaultProvider())
+ .putList(
+ "listAttributeName",
+ Arrays.asList("listAttributeValue1", "listAttributeValue2"),
+ EnhancedType.of(String.class))
+ .build();
+
+ List result = document.getListOfUnknownType("listAttributeName");
+
+ assertThat(result).isNotNull();
+ assertThat(result).hasSize(2);
+ assertThat(result)
+ .containsExactlyInAnyOrder(
+ AttributeValue.builder().s("listAttributeValue1").build(),
+ AttributeValue.builder().s("listAttributeValue2").build());
+ }
+
+ @Test
+ void getListOfUnknownType_forNullAttributeName_throwsException() {
+ DefaultEnhancedDocument document = (DefaultEnhancedDocument) defaultDocBuilder()
+ .attributeConverterProviders(defaultProvider())
+ .putNull("nullAttributeName")
+ .build();
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> document.getListOfUnknownType("nullAttributeName"))
+ .withMessageContaining("Cannot get a List from attribute value of Type NUL");
+ }
+
+ @Test
+ void getListOfUnknownType_forStringAttributeName_throwsException() {
+ DefaultEnhancedDocument document = (DefaultEnhancedDocument) defaultDocBuilder()
+ .attributeConverterProviders(defaultProvider())
+ .putString("stringAttributeName", "stringAttributeValue")
+ .build();
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> document.getListOfUnknownType("stringAttributeName"))
+ .withMessageContaining("Cannot get a List from attribute value of Type S");
+ }
+
+ @Test
+ void getMapOfUnknownType_forUnknownAttributeName_returnsNull() {
+ DefaultEnhancedDocument document = (DefaultEnhancedDocument) defaultDocBuilder()
+ .attributeConverterProviders(defaultProvider())
+ .putNull("nullAttributeName")
+ .putString("attributeName", "attributeValue")
+ .build();
+
+ Map result = document.getMapOfUnknownType("unknownAttributeName");
+ assertThat(result).isNull();
+ }
+
+ @Test
+ void getMapOfUnknownType_forMapAttributeName_returnsCorrectValue() {
+ Map innerMap = new HashMap<>();
+ innerMap.put("innerMapKey1", "innerMapValue1");
+ innerMap.put("innerMapKey2", "innerMapValue2");
+
+ DefaultEnhancedDocument document = (DefaultEnhancedDocument) defaultDocBuilder()
+ .attributeConverterProviders(defaultProvider())
+ .putMap(
+ "mapAttributeName",
+ innerMap,
+ EnhancedType.of(String.class),
+ EnhancedType.of(String.class))
+ .build();
+
+ Map result = document.getMapOfUnknownType("mapAttributeName");
+
+ assertThat(result).isNotNull();
+ assertThat(result).hasSize(2);
+ assertThat(result.get("innerMapKey1").s()).isEqualTo("innerMapValue1");
+ assertThat(result.get("innerMapKey2").s()).isEqualTo("innerMapValue2");
+ }
+
+ @Test
+ void getMapOfUnknownType_forNullAttributeName_throwsException() {
+ DefaultEnhancedDocument document = (DefaultEnhancedDocument) defaultDocBuilder()
+ .attributeConverterProviders(defaultProvider())
+ .putNull("nullAttributeName")
+ .build();
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> document.getMapOfUnknownType("nullAttributeName"))
+ .withMessageContaining("Cannot get a Map from attribute value of Type NUL");
+ }
+
+ @Test
+ void getMapOfUnknownType_forStringAttributeName_throwsException() {
+ DefaultEnhancedDocument document = (DefaultEnhancedDocument) defaultDocBuilder()
+ .attributeConverterProviders(defaultProvider())
+ .putString("stringAttributeName", "stringAttributeValue")
+ .build();
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> document.getMapOfUnknownType("stringAttributeName"))
+ .withMessageContaining("Cannot get a Map from attribute value of Type S");
+ }
+
+ @Test
+ void putStringSet_onNullValue_throwsException() {
+ DefaultEnhancedDocument.DefaultBuilder builder =
+ (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder()
+ .attributeConverterProviders(defaultProvider());
+ Set values = new LinkedHashSet<>(Arrays.asList("a", null, "b"));
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> builder.putStringSet("stringSet", values))
+ .withMessage("Set must not have null values.");
+ }
+
+ @Test
+ void putNumberSet_onNullValue_throwsException() {
+ DefaultEnhancedDocument.DefaultBuilder builder =
+ (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder()
+ .attributeConverterProviders(defaultProvider());
+ Set values = new LinkedHashSet<>(Arrays.asList(1, null, 2));
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> builder.putNumberSet("numberSet", values))
+ .withMessage("Set must not have null values.");
+ }
+
+ @Test
+ void putBytesSet_onNullValue_throwsException() {
+ DefaultEnhancedDocument.DefaultBuilder builder =
+ (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder()
+ .attributeConverterProviders(defaultProvider());
+ Set values = new LinkedHashSet<>(Arrays.asList(SdkBytes.fromUtf8String("a"), null));
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> builder.putBytesSet("bytesSet", values))
+ .withMessage("Set must not have null values.");
+ }
+
+ @Test
+ void toJson_onEmptyDocument_returnsEmptyJson() {
+ DefaultEnhancedDocument doc = (DefaultEnhancedDocument) DefaultEnhancedDocument.builder().build();
+ assertThat(doc.toJson()).isEqualTo("{}");
+ }
+
+ @Test
+ void toJson_onNonEmptyDocument_returnsJsonWithKeyAndValue() {
+ DefaultEnhancedDocument doc = (DefaultEnhancedDocument)
+ DefaultEnhancedDocument.builder()
+ .putString("key", "value")
+ .attributeConverterProviders(defaultProvider())
+ .build();
+ assertThat(doc.toJson()).contains("key");
+ assertThat(doc.toJson()).contains("value");
+ }
+
+ @Test
+ void putStringSet_onValidSet_addsStringSet() {
+ DefaultEnhancedDocument.DefaultBuilder builder =
+ (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder()
+ .attributeConverterProviders(defaultProvider());
+ Set values = new LinkedHashSet<>(Arrays.asList("a", "b"));
+
+ builder.putStringSet("stringSet", values);
+
+ DefaultEnhancedDocument doc = (DefaultEnhancedDocument) builder.build();
+ assertThat(doc.toMap().get("stringSet").ss()).containsExactlyInAnyOrder("a", "b");
+ }
+
+ @Test
+ void putNumberSet_onValidSet_addsNumberSet() {
+ DefaultEnhancedDocument.DefaultBuilder builder = (DefaultEnhancedDocument.DefaultBuilder)
+ DefaultEnhancedDocument.builder().attributeConverterProviders(defaultProvider());
+ Set values = new LinkedHashSet<>(Arrays.asList(1, 2));
+
+ builder.putNumberSet("numberSet", values);
+
+ DefaultEnhancedDocument doc = (DefaultEnhancedDocument) builder.build();
+ assertThat(doc.toMap().get("numberSet").ns()).containsExactlyInAnyOrder("1", "2");
+ }
+
+ @Test
+ void putBytesSet_onValidSet_addsBytesSet() {
+ DefaultEnhancedDocument.DefaultBuilder builder =
+ (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder()
+ .attributeConverterProviders(defaultProvider());
+ Set values = new LinkedHashSet<>(Arrays.asList(SdkBytes.fromUtf8String("a"), SdkBytes.fromUtf8String("b")));
+
+ builder.putBytesSet("bytesSet", values);
+
+ DefaultEnhancedDocument doc = (DefaultEnhancedDocument) builder.build();
+ assertThat(doc.toMap().get("bytesSet").bs()).hasSize(2);
+ }
+
+ @Test
+ void json_onValidJson_setsAttributeValueMap() {
+ String json = "{\"foo\":{\"S\":\"bar\"}}";
+ DefaultEnhancedDocument.DefaultBuilder builder =
+ (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder();
+
+ builder.json(json);
+
+ DefaultEnhancedDocument doc = (DefaultEnhancedDocument) builder.build();
+ assertThat(doc.toMap()).containsKey("foo");
+ assertThat(doc.toMap().get("foo").m().get("S").s()).isEqualTo("bar");
+ }
+
+ @Test
+ void json_onInvalidJson_throwsUncheckedIOException() {
+ DefaultEnhancedDocument.DefaultBuilder builder =
+ (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder();
+
+ assertThatThrownBy(() -> builder.json("not a json"))
+ .isInstanceOf(java.io.UncheckedIOException.class)
+ .hasMessageContaining("Unrecognized token");
+ }
+
+ @Test
+ void json_onJsonParsingToNull_throwsIllegalArgumentException() {
+ DefaultEnhancedDocument.DefaultBuilder builder =
+ (DefaultEnhancedDocument.DefaultBuilder) DefaultEnhancedDocument.builder();
+
+ assertThatThrownBy(() -> builder.json(""))
+ .isInstanceOf(java.lang.IllegalArgumentException.class)
+ .hasMessageContaining("Could not parse argument json");
+ assertThatThrownBy(() -> builder.json(" "))
+ .isInstanceOf(java.lang.IllegalArgumentException.class)
+ .hasMessageContaining("Could not parse argument json");
+ }
}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentTableSchemaTest.java
index dcafaae6c66d..79e26f3b6b72 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentTableSchemaTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/DocumentTableSchemaTest.java
@@ -16,30 +16,30 @@
package software.amazon.awssdk.enhanced.dynamodb.document;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
-import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider;
import static software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData.testDataInstance;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
+import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider;
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
-import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomAttributeForDocumentConverterProvider;
import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI;
-import software.amazon.awssdk.enhanced.dynamodb.document.DocumentTableSchema;
-import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument;
-import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData;
-import software.amazon.awssdk.enhanced.dynamodb.document.TestData;
import software.amazon.awssdk.enhanced.dynamodb.internal.converter.ChainConverterProvider;
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticKeyAttributeMetadata;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
@@ -49,6 +49,39 @@ class DocumentTableSchemaTest {
String NO_PRIMARY_KEYS_IN_METADATA = "Attempt to execute an operation that requires a primary index without defining "
+ "any primary key attributes in the table metadata.";
+ @Test
+ void attributeValue_forNullItem_returnsNull() {
+ DocumentTableSchema documentTableSchema =
+ DocumentTableSchema.builder()
+ .attributeConverterProviders(defaultProvider())
+ .build();
+
+ assertThat(documentTableSchema.attributeValue(null, "key")).isNull();
+ }
+
+ @Test
+ void itemToMapWithIgnoreNullsFlag_forNullItem_returnsNull() {
+ DocumentTableSchema documentTableSchema =
+ DocumentTableSchema.builder()
+ .attributeConverterProviders(defaultProvider())
+ .build();
+
+ assertThat(documentTableSchema.itemToMap(null, false)).isNull();
+ }
+
+ @Test
+ void itemToMap_withListOfAttributes_forItemToMapNull_returnsNull() {
+ DocumentTableSchema documentTableSchema =
+ DocumentTableSchema.builder()
+ .attributeConverterProviders(defaultProvider())
+ .build();
+
+ EnhancedDocument doc = mock(EnhancedDocument.class);
+ when(doc.toMap()).thenReturn(null);
+
+ assertThat(documentTableSchema.itemToMap(doc, Arrays.asList("filterOne", "filterTwo"))).isNull();
+ }
+
@Test
void converterForAttribute_APIIsNotSupported() {
DocumentTableSchema documentTableSchema = DocumentTableSchema.builder().build();
@@ -237,4 +270,49 @@ void validate_DocumentTableSchema_WithCustomIntegerAttributeProvider() {
Assertions.assertThat(
documentTableSchema.itemToMap(numberDocument, true)).isEqualTo(resultMap);
}
-}
+
+ @Test
+ void mergeAttributeConverterProviders_withItemHavingConverters_mergesProviders() {
+ DocumentTableSchema schema = DocumentTableSchema.builder().build();
+
+ EnhancedDocument mockItem = mock(EnhancedDocument.class);
+ EnhancedDocument.Builder mockBuilder = mock(EnhancedDocument.Builder.class);
+ EnhancedDocument builtItem = mock(EnhancedDocument.class);
+
+ when(mockItem.attributeConverterProviders()).thenReturn(Arrays.asList(defaultProvider()));
+ when(mockItem.toBuilder()).thenReturn(mockBuilder);
+ when(mockBuilder.attributeConverterProviders((List) any()))
+ .thenReturn(mockBuilder);
+ when(mockBuilder.build()).thenReturn(builtItem);
+ Map resultMap = Collections.singletonMap("key", AttributeValue.fromS("value"));
+ when(builtItem.toMap()).thenReturn(resultMap);
+
+ Map result = schema.itemToMap(mockItem, false);
+
+ assertThat(result).containsKey("key");
+ }
+
+ @Test
+ void itemToMapWithAttributes_duplicateKeys_keepsFirstValue() {
+ DocumentTableSchema schema = DocumentTableSchema.builder().build();
+
+ EnhancedDocument mockItem = mock(EnhancedDocument.class);
+ EnhancedDocument.Builder mockBuilder = mock(EnhancedDocument.Builder.class);
+ EnhancedDocument builtItem = mock(EnhancedDocument.class);
+ Map itemMap = new LinkedHashMap<>();
+ itemMap.put("key1", AttributeValue.fromS("value1"));
+ itemMap.put("key2", AttributeValue.fromS("value2"));
+
+ when(mockItem.toMap()).thenReturn(itemMap);
+ when(mockItem.attributeConverterProviders()).thenReturn(null);
+ when(mockItem.toBuilder()).thenReturn(mockBuilder);
+ when(mockBuilder.attributeConverterProviders((List) any()))
+ .thenReturn(mockBuilder);
+ when(mockBuilder.build()).thenReturn(builtItem);
+ when(builtItem.toMap()).thenReturn(itemMap);
+
+ Map result = schema.itemToMap(mockItem, Arrays.asList("key1", "key1"));
+
+ assertThat(result).hasSize(1).containsKey("key1");
+ }
+}
\ No newline at end of file
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtensionTest.java
new file mode 100644
index 000000000000..2b67beef058d
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtensionTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.enhanced.dynamodb.extensions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import org.junit.jupiter.api.Test;
+
+class AutoGeneratedTimestampRecordExtensionTest {
+
+ @Test
+ void toBuilder_preservesClock() {
+ Clock customClock = Clock.fixed(Instant.parse("2025-01-01T00:00:00Z"), ZoneOffset.UTC);
+ AutoGeneratedTimestampRecordExtension extension =
+ AutoGeneratedTimestampRecordExtension.builder()
+ .baseClock(customClock)
+ .build();
+
+ AutoGeneratedTimestampRecordExtension.Builder rebuiltExtensionBuilder = extension.toBuilder();
+ AutoGeneratedTimestampRecordExtension rebuiltExtension = rebuiltExtensionBuilder.build();
+
+ assertThat(rebuiltExtension).isNotNull();
+ assertThat(rebuiltExtension).usingRecursiveComparison().isEqualTo(extension);
+ }
+
+ @Test
+ void constructor_withNullClock_usesSystemUTC() {
+ AutoGeneratedTimestampRecordExtension extension = AutoGeneratedTimestampRecordExtension.builder().build();
+
+ assertThat(extension).isNotNull();
+ }
+}
\ No newline at end of file
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java
index cc69f503d50f..0a48d5f0ba55 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java
@@ -45,7 +45,7 @@ public class AutoGeneratedUuidExtensionTest {
private static final OperationContext PRIMARY_CONTEXT =
DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName());
- private final AutoGeneratedUuidExtension atomicCounterExtension = AutoGeneratedUuidExtension.create();
+ private final AutoGeneratedUuidExtension uuidExtension = AutoGeneratedUuidExtension.create();
private static final StaticTableSchema ITEM_WITH_UUID_MAPPER =
@@ -65,6 +65,32 @@ public class AutoGeneratedUuidExtensionTest {
.setter(ItemWithUuid::setSimpleString))
.build();
+ @Test
+ public void beforeWrite_schemaWithoutUuidAttribute_returnsEmptyWriteModification() {
+ StaticTableSchema schemaWithoutUuidMetadata =
+ StaticTableSchema.builder(ItemWithUuid.class)
+ .newItemSupplier(ItemWithUuid::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(ItemWithUuid::getId)
+ .setter(ItemWithUuid::setId)
+ .addTag(primaryPartitionKey()))
+ .build();
+
+ ItemWithUuid item = new ItemWithUuid();
+ item.setId(RECORD_ID);
+ Map items = schemaWithoutUuidMetadata.itemToMap(item, true);
+
+ WriteModification result = uuidExtension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(schemaWithoutUuidMetadata.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
+
+ assertThat(result).usingRecursiveComparison().isEqualTo(WriteModification.builder().build());
+ }
+
@Test
public void beforeWrite_updateItemOperation_hasUuidInItem_doesNotCreateUpdateExpressionAndFilters() {
ItemWithUuid SimpleItem = new ItemWithUuid();
@@ -76,11 +102,11 @@ public void beforeWrite_updateItemOperation_hasUuidInItem_doesNotCreateUpdateExp
assertThat(items).hasSize(2);
WriteModification result =
- atomicCounterExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
- .items(items)
- .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata())
- .operationName(OperationName.UPDATE_ITEM)
- .operationContext(PRIMARY_CONTEXT).build());
+ uuidExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata())
+ .operationName(OperationName.UPDATE_ITEM)
+ .operationContext(PRIMARY_CONTEXT).build());
Map transformedItem = result.transformedItem();
assertThat(transformedItem).isNotNull().hasSize(2);
@@ -99,11 +125,11 @@ public void beforeWrite_updateItemOperation_hasNoUuidInItem_doesNotCreatesUpdate
assertThat(items).hasSize(1);
WriteModification result =
- atomicCounterExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
- .items(items)
- .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata())
- .operationName(OperationName.UPDATE_ITEM)
- .operationContext(PRIMARY_CONTEXT).build());
+ uuidExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata())
+ .operationName(OperationName.UPDATE_ITEM)
+ .operationContext(PRIMARY_CONTEXT).build());
Map transformedItem = result.transformedItem();
assertThat(transformedItem).isNotNull().hasSize(2);
@@ -121,11 +147,11 @@ public void beforeWrite_updateItemOperation_UuidNotPresent_newUuidCreated() {
assertThat(items).hasSize(1);
WriteModification result =
- atomicCounterExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
- .items(items)
- .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata())
- .operationName(OperationName.UPDATE_ITEM)
- .operationContext(PRIMARY_CONTEXT).build());
+ uuidExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata())
+ .operationName(OperationName.UPDATE_ITEM)
+ .operationContext(PRIMARY_CONTEXT).build());
assertThat(result.transformedItem()).isNotNull();
assertThat(result.updateExpression()).isNull();
assertThat(result.transformedItem()).hasSize(2);
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java
index 3f30fdc8ecdf..d6803a88d7d9 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java
@@ -15,16 +15,19 @@
package software.amazon.awssdk.enhanced.dynamodb.extensions;
+import static org.assertj.core.api.Assertions.assertThat;
import static java.util.Collections.singletonMap;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension.AttributeTags.versionAttribute;
import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem.createUniqueFakeItem;
import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort.createUniqueFakeItemWithSort;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
+import java.util.function.BiConsumer;
import java.util.stream.Stream;
import org.junit.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -41,6 +44,8 @@
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeVersionedStaticImmutableItem;
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
@@ -64,7 +69,7 @@ public void beforeRead_doesNotTransformObject() {
.tableMetadata(FakeItem.getTableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result, is(ReadModification.builder().build()));
+ assertThat(result).isEqualTo(ReadModification.builder().build());
}
@Test
@@ -79,11 +84,10 @@ public void beforeWrite_initialVersion_expressionIsCorrect() {
.tableMetadata(FakeItem.getTableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.additionalConditionalExpression(),
- is(Expression.builder()
+ assertThat(result.additionalConditionalExpression()).isEqualTo(Expression.builder()
.expression("attribute_not_exists(#AMZN_MAPPED_version)")
.expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
- .build()));
+ .build());
}
@Test
@@ -101,7 +105,7 @@ public void beforeWrite_initialVersion_transformedItemIsCorrect() {
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.transformedItem(), is(fakeItemWithInitialVersion));
+ assertThat(result.transformedItem()).isEqualTo(fakeItemWithInitialVersion);
}
@Test
@@ -121,7 +125,7 @@ public void beforeWrite_initialVersionDueToExplicitNull_transformedItemIsCorrect
.tableMetadata(FakeItem.getTableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.transformedItem(), is(fakeItemWithInitialVersion));
+ assertThat(result.transformedItem()).isEqualTo(fakeItemWithInitialVersion);
}
@Test
@@ -136,13 +140,12 @@ public void beforeWrite_existingVersion_expressionIsCorrect() {
.tableMetadata(FakeItem.getTableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.additionalConditionalExpression(),
- is(Expression.builder()
+ assertThat(result.additionalConditionalExpression()).isEqualTo(Expression.builder()
.expression("#AMZN_MAPPED_version = :old_version_value")
.expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
.expressionValues(singletonMap(":old_version_value",
AttributeValue.builder().n("13").build()))
- .build()));
+ .build());
}
@Test
@@ -160,7 +163,7 @@ public void beforeWrite_existingVersion_transformedItemIsCorrect() {
.tableMetadata(FakeItem.getTableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.transformedItem(), is(fakeItemWithInitialVersion));
+ assertThat(result.transformedItem()).isEqualTo(fakeItemWithInitialVersion);
}
@Test
@@ -174,7 +177,7 @@ public void beforeWrite_returnsNoOpModification_ifVersionAttributeNotDefined() {
.operationContext(PRIMARY_CONTEXT)
.tableMetadata(FakeItemWithSort.getTableMetadata())
.build());
- assertThat(writeModification, is(WriteModification.builder().build()));
+ assertThat(writeModification).isEqualTo(WriteModification.builder().build());
}
@Test(expected = IllegalArgumentException.class)
@@ -211,8 +214,7 @@ public void beforeWrite_versionEqualsStartAt_treatedAsInitialVersion() {
.tableMetadata(FakeItem.getTableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.additionalConditionalExpression().expression(),
- is("attribute_not_exists(#AMZN_MAPPED_version) OR #AMZN_MAPPED_version = :old_version_value"));
+ assertThat(result.additionalConditionalExpression().expression()).isEqualTo("attribute_not_exists(#AMZN_MAPPED_version) OR #AMZN_MAPPED_version = :old_version_value");
}
@ParameterizedTest
@@ -245,12 +247,11 @@ public void customStartingValueAndIncrement_worksAsExpected(Long startAt, Long i
.tableMetadata(FakeItem.getTableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.transformedItem(), is(expectedInitialVersion));
- assertThat(result.additionalConditionalExpression(),
- is(Expression.builder()
+ assertThat(result.transformedItem()).isEqualTo(expectedInitialVersion);
+ assertThat(result.additionalConditionalExpression()).isEqualTo(Expression.builder()
.expression("attribute_not_exists(#AMZN_MAPPED_version)")
.expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
- .build()));
+ .build());
}
public static Stream customStartAtAndIncrementValues() {
@@ -273,7 +274,32 @@ public void customStartingValueAndIncrement_shouldThrow(Long startAt, Long incre
public static Stream customFailingStartAtAndIncrementValues() {
return Stream.of(
Arguments.of(-2L, 1L),
- Arguments.of(3L, 0L));
+ Arguments.of(3L, 0L),
+ Arguments.of(-1L, 0L));
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("invalidBuilderSetterArguments")
+ public void builder_invalidSetter_throwsIllegalArgumentException(
+ String caseDescription,
+ BiConsumer setter,
+ long invalidValue) {
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ VersionedRecordExtension.Builder builder = VersionedRecordExtension.builder();
+ setter.accept(builder, invalidValue);
+ builder.build();
+ });
+ }
+
+ private static Stream invalidBuilderSetterArguments() {
+ BiConsumer startAt = VersionedRecordExtension.Builder::startAt;
+ BiConsumer incrementBy = VersionedRecordExtension.Builder::incrementBy;
+ return Stream.of(
+ Arguments.of("startAt(-2)", startAt, -2L),
+ Arguments.of("startAt(MIN_VALUE)", startAt, Long.MIN_VALUE),
+ Arguments.of("incrementBy(0)", incrementBy, 0L),
+ Arguments.of("incrementBy(-1)", incrementBy, -1L));
}
@Test
@@ -296,8 +322,7 @@ public void beforeWrite_versionNotEqualsAnnotationStartAt_notTreatedAsInitialVer
.tableMetadata(schema.tableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.additionalConditionalExpression().expression(),
- is("#AMZN_MAPPED_version = :old_version_value"));
+ assertThat(result.additionalConditionalExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :old_version_value");
}
@Test
@@ -320,8 +345,7 @@ public void beforeWrite_versionEqualsAnnotationStartAt_isTreatedAsInitialVersion
.tableMetadata(schema.tableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.additionalConditionalExpression().expression(),
- is("attribute_not_exists(#AMZN_MAPPED_version) OR #AMZN_MAPPED_version = :old_version_value"));
+ assertThat(result.additionalConditionalExpression().expression()).isEqualTo("attribute_not_exists(#AMZN_MAPPED_version) OR #AMZN_MAPPED_version = :old_version_value");
}
@@ -364,12 +388,11 @@ public void customStartingValueAndIncrementWithAnnotation_worksAsExpected() {
.tableMetadata(schema.tableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.transformedItem(), is(expectedInitialVersion));
- assertThat(result.additionalConditionalExpression(),
- is(Expression.builder()
+ assertThat(result.transformedItem()).isEqualTo(expectedInitialVersion);
+ assertThat(result.additionalConditionalExpression()).isEqualTo(Expression.builder()
.expression("attribute_not_exists(#AMZN_MAPPED_version)")
.expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
- .build()));
+ .build());
}
@Test
@@ -396,12 +419,11 @@ public void customAnnotationValuesAndBuilderValues_annotationShouldTakePrecedenc
.tableMetadata(schema.tableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.transformedItem(), is(expectedInitialVersion));
- assertThat(result.additionalConditionalExpression(),
- is(Expression.builder()
+ assertThat(result.transformedItem()).isEqualTo(expectedInitialVersion);
+ assertThat(result.additionalConditionalExpression()).isEqualTo(Expression.builder()
.expression("attribute_not_exists(#AMZN_MAPPED_version)")
.expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
- .build()));
+ .build());
}
@DynamoDbBean
@@ -445,12 +467,11 @@ public void customAnnotationDefaultValuesAndBuilderValues_annotationShouldTakePr
.tableMetadata(schema.tableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.transformedItem(), is(expectedInitialVersion));
- assertThat(result.additionalConditionalExpression(),
- is(Expression.builder()
+ assertThat(result.transformedItem()).isEqualTo(expectedInitialVersion);
+ assertThat(result.additionalConditionalExpression()).isEqualTo(Expression.builder()
.expression("attribute_not_exists(#AMZN_MAPPED_version)")
.expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
- .build()));
+ .build());
}
@DynamoDbBean
@@ -508,9 +529,8 @@ public void customIncrementForExistingVersion_worksAsExpected(Long startAt, Long
.tableMetadata(FakeItem.getTableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.transformedItem(), is(expectedVersionedItem));
- assertThat(result.additionalConditionalExpression().expression(),
- is("#AMZN_MAPPED_version = :old_version_value"));
+ assertThat(result.transformedItem()).isEqualTo(expectedVersionedItem);
+ assertThat(result.additionalConditionalExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :old_version_value");
}
@ParameterizedTest
@@ -546,9 +566,8 @@ public void customIncrementForExistingVersion_withImmutableSchema_worksAsExpecte
.tableMetadata(FakeVersionedStaticImmutableItem.getTableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.transformedItem(), is(expectedVersionedItem));
- assertThat(result.additionalConditionalExpression().expression(),
- is("#AMZN_MAPPED_version = :old_version_value"));
+ assertThat(result.transformedItem()).isEqualTo(expectedVersionedItem);
+ assertThat(result.additionalConditionalExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :old_version_value");
}
@Test
@@ -574,12 +593,11 @@ public void customStartingValueAndIncrementWithImmutableClass_worksAsExpected()
.tableMetadata(schema.tableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.transformedItem(), is(expectedInitialVersion));
- assertThat(result.additionalConditionalExpression(),
- is(Expression.builder()
+ assertThat(result.transformedItem()).isEqualTo(expectedInitialVersion);
+ assertThat(result.additionalConditionalExpression()).isEqualTo(Expression.builder()
.expression("attribute_not_exists(#AMZN_MAPPED_version)")
.expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
- .build()));
+ .build());
}
@Test(expected = IllegalStateException.class)
@@ -630,8 +648,7 @@ public void isInitialVersion_shouldPrioritizeAnnotationValueOverBuilderValue() {
.tableMetadata(schema.tableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.additionalConditionalExpression().expression(),
- is("#AMZN_MAPPED_version = :old_version_value"));
+ assertThat(result.additionalConditionalExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :old_version_value");
}
@Test
@@ -649,8 +666,7 @@ public void updateItem_existingRecordWithVersionEqualToStartAt_shouldSucceed() {
.tableMetadata(FakeItem.getTableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.additionalConditionalExpression().expression(),
- is("attribute_not_exists(#AMZN_MAPPED_version) OR #AMZN_MAPPED_version = :old_version_value"));
+ assertThat(result.additionalConditionalExpression().expression()).isEqualTo("attribute_not_exists(#AMZN_MAPPED_version) OR #AMZN_MAPPED_version = :old_version_value");
}
@Test
@@ -671,7 +687,7 @@ public void beforeWrite_startAtNegativeOne_firstVersionIsZero() {
.tableMetadata(FakeItem.getTableMetadata())
.operationContext(PRIMARY_CONTEXT).build());
- assertThat(result.transformedItem(), is(expectedItem));
+ assertThat(result.transformedItem()).isEqualTo(expectedItem);
}
public static Stream customIncrementForExistingVersionValues() {
@@ -681,4 +697,80 @@ public static Stream customIncrementForExistingVersionValues() {
Arguments.of(3L, null, 10L, "11"),
Arguments.of(null, 3L, 4L, "7"));
}
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("invalidVersionAttributeTagArguments")
+ public void versionAttribute_withInvalidStartAtOrIncrementBy_throwsIllegalArgumentException(
+ String caseDescription,
+ long startAt,
+ long incrementBy,
+ String expectedMessage) {
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> buildTestItemStaticSchemaWithLongVersion(versionAttribute(startAt, incrementBy)))
+ .withMessage(expectedMessage);
+ }
+
+ private static Stream invalidVersionAttributeTagArguments() {
+ return Stream.of(
+ Arguments.of("invalid startAt", -2L, 1L, "startAt must be -1 or greater."),
+ Arguments.of("invalid incrementBy", 0L, 0L, "incrementBy must be greater than 0."));
+ }
+
+ @Test
+ public void versionAttribute_withNonNumericType_throwsIllegalArgumentException() {
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() ->
+ testItemStaticSchemaBuilderThroughPk()
+ .addAttribute(String.class,
+ a -> a.name("version")
+ .getter(TestItem::getId)
+ .setter(TestItem::setId)
+ .addTag(versionAttribute()))
+ .build()
+ )
+ .withMessageContaining(
+ "is not a suitable type to be used as a version attribute. Only type 'N' is supported.");
+ }
+
+ private static StaticTableSchema.Builder testItemStaticSchemaBuilderThroughPk() {
+ return StaticTableSchema.builder(TestItem.class)
+ .newItemSupplier(TestItem::new)
+ .addAttribute(String.class,
+ a -> a.name("id")
+ .getter(TestItem::getId)
+ .setter(TestItem::setId)
+ .addTag(primaryPartitionKey()));
+ }
+
+ private static void buildTestItemStaticSchemaWithLongVersion(StaticAttributeTag versionTag) {
+ testItemStaticSchemaBuilderThroughPk()
+ .addAttribute(Long.class,
+ a -> a.name("version")
+ .getter(TestItem::getVersion)
+ .setter(TestItem::setVersion)
+ .addTag(versionTag))
+ .build();
+ }
+
+ private static class TestItem {
+ private String id;
+ private Long version;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public Long getVersion() {
+ return version;
+ }
+
+ public void setVersion(Long version) {
+ this.version = version;
+ }
+ }
}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedImmutableTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedImmutableTableSchemaTest.java
deleted file mode 100644
index 998f13998280..000000000000
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedImmutableTableSchemaTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.enhanced.dynamodb.functionaltests;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import org.junit.After;
-import org.junit.Test;
-import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
-import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
-import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
-import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.ImmutableFakeItem;
-import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest;
-import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput;
-
-public class AnnotatedImmutableTableSchemaTest extends LocalDynamoDbSyncTestBase {
- private static final String TABLE_NAME = "table-name";
-
- private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder()
- .dynamoDbClient(getDynamoDbClient())
- .build();
-
- @After
- public void deleteTable() {
- getDynamoDbClient().deleteTable(DeleteTableRequest.builder()
- .tableName(getConcreteTableName(TABLE_NAME))
- .build());
- }
-
- @Test
- public void simpleItem_putAndGet() {
- TableSchema tableSchema =
- TableSchema.fromClass(ImmutableFakeItem.class);
-
- DynamoDbTable mappedTable =
- enhancedClient.table(getConcreteTableName(TABLE_NAME), tableSchema);
-
- mappedTable.createTable(r -> r.provisionedThroughput(ProvisionedThroughput.builder()
- .readCapacityUnits(5L)
- .writeCapacityUnits(5L)
- .build()));
- ImmutableFakeItem immutableFakeItem = ImmutableFakeItem.builder()
- .id("id123")
- .attribute("test-value")
- .build();
-
- mappedTable.putItem(immutableFakeItem);
- ImmutableFakeItem readItem = mappedTable.getItem(immutableFakeItem);
- assertThat(readItem).isEqualTo(immutableFakeItem);
- }
-}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedTableSchemaTest.java
new file mode 100644
index 000000000000..a2365fd03b1f
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedTableSchemaTest.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.enhanced.dynamodb.functionaltests;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue;
+
+import java.util.Collection;
+import java.util.function.Function;
+
+import org.assertj.core.api.Assertions;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
+import software.amazon.awssdk.enhanced.dynamodb.Expression;
+import software.amazon.awssdk.enhanced.dynamodb.Key;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest;
+import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedResponse;
+import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTableEnhancedResponse;
+import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedResponse;
+import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest;
+import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedResponse;
+import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
+import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
+import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
+import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest;
+import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException;
+import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
+import software.amazon.awssdk.services.dynamodb.model.TableDescription;
+
+@RunWith(Parameterized.class)
+public class AnnotatedTableSchemaTest extends LocalDynamoDbSyncTestBase {
+
+ private static final String TABLE_NAME = "table-name";
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection