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/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 parameters() { + return AnnotatedTableSchemaTestSupport.parameters(); + } + + @Parameterized.Parameter(0) + public String schemaType; + + @Parameterized.Parameter(1) + public Class itemClass; + + @Parameterized.Parameter(2) + public TableSchema tableSchema; + + @Parameterized.Parameter(3) + public Function fullItem; + + @Parameterized.Parameter(4) + public Function partialItem; + + @Parameterized.Parameter(5) + public Function firstItem; + + @Parameterized.Parameter(6) + public Function secondItem; + + @Parameterized.Parameter(7) + public Function updatedItem; + + @Parameterized.Parameter(8) + public Function updatedItemWithNullString; + + @Parameterized.Parameter(9) + public Class abstractItemClass; + + private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .build(); + + private DynamoDbTable mappedTable; + private AnnotatedTableSchemaTestSupport.TestItemFactory factory; + + @Before + public void createTable() { + mappedTable = enhancedClient.table(getConcreteTableName(TABLE_NAME), tableSchema); + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + factory = new AnnotatedTableSchemaTestSupport.TestItemFactory(); + } + + @After + public void deleteTable() { + try { + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(TABLE_NAME)) + .build()); + } catch (ResourceNotFoundException ignored) { + // Table doesn't exist, nothing to delete + } + } + + @Test + public void describeTable_succeeds() { + DescribeTableEnhancedResponse describeTableEnhancedResponse = mappedTable.describeTable(); + Assertions.assertThat(describeTableEnhancedResponse.table()).isNotNull(); + Assertions.assertThat(describeTableEnhancedResponse.table().tableName()) + .isEqualTo(getConcreteTableName(TABLE_NAME)); + } + + @Test + public void createTableWithDefaults_thenDeleteTable_succeeds() { + String tableName = TABLE_NAME + "-1"; + + DynamoDbTable anotherMappedTable = enhancedClient.table(getConcreteTableName(tableName), tableSchema); + anotherMappedTable.createTable(); + + TableDescription tableDescription = anotherMappedTable.describeTable().table(); + + String actualTableName = tableDescription.tableName(); + Long actualReadCapacityUnits = tableDescription.provisionedThroughput().readCapacityUnits(); + Long actualWriteCapacityUnits = tableDescription.provisionedThroughput().writeCapacityUnits(); + + assertThat(actualTableName).isEqualTo(getConcreteTableName(tableName)); + assertThat(actualReadCapacityUnits).isEqualTo(0L); + assertThat(actualWriteCapacityUnits).isEqualTo(0L); + + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(tableName)) + .build()); + + assertThatThrownBy(anotherMappedTable::describeTable) + .isInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("Cannot do operations on a non-existent table"); + } + + @Test + public void createTableWithProvisionedThroughput_succeeds() { + String tableName = TABLE_NAME + "-1"; + + DynamoDbTable anotherMappedTable = enhancedClient.table(getConcreteTableName(tableName), tableSchema); + anotherMappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + TableDescription tableDescription = anotherMappedTable.describeTable().table(); + + String actualTableName = tableDescription.tableName(); + Long actualReadCapacityUnits = tableDescription.provisionedThroughput().readCapacityUnits(); + Long actualWriteCapacityUnits = tableDescription.provisionedThroughput().writeCapacityUnits(); + + assertThat(actualTableName).isEqualTo(getConcreteTableName(tableName)); + assertThat(actualReadCapacityUnits).isEqualTo(getDefaultProvisionedThroughput().readCapacityUnits()); + assertThat(actualWriteCapacityUnits).isEqualTo(getDefaultProvisionedThroughput().writeCapacityUnits()); + + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(tableName)) + .build()); + + assertThatThrownBy(anotherMappedTable::describeTable) + .isInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("Cannot do operations on a non-existent table"); + } + + @Test + public void createTableWithDefaults_throwsIllegalArgumentException() { + TableSchema badSchema = TableSchema.fromClass(abstractItemClass); + DynamoDbTable other = enhancedClient.table(getConcreteTableName(TABLE_NAME), badSchema); + + assertThatThrownBy(other::createTable) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Attempt to execute an operation that requires a primary index without defining any primary" + + " key attributes in the table metadata."); + } + + @Test + public void createTableWithProvisionedThroughput_throwsIllegalArgumentException() { + TableSchema badSchema = TableSchema.fromClass(abstractItemClass); + DynamoDbTable other = enhancedClient.table(getConcreteTableName(TABLE_NAME), badSchema); + + assertThatThrownBy(() -> other.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Attempt to execute an operation that requires a primary index without defining any primary" + + " key attributes in the table metadata."); + } + + @Test + public void getItem_itemNotFound_returnsNullValue() { + Object item = fullItem.apply(factory); + + Object result = mappedTable.getItem(item); + assertThat(result).isNull(); + } + + @Test + public void getItemWithResponse_itemNotFound_returnsNullValue() { + GetItemEnhancedResponse getItemEnhancedResponse = + mappedTable.getItemWithResponse(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + + assertThat(getItemEnhancedResponse.attributes()).isNull(); + assertThat(getItemEnhancedResponse.consumedCapacity()).isNull(); + } + + @Test + public void putItem_thenGetItem_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item); + + Object result = mappedTable.getItem(item); + assertThat(result).isEqualTo(item); + } + + @Test + public void putItemPartial_thenGetItem_succeeds() { + Object item = partialItem.apply(factory); + mappedTable.putItem(item); + + Object result = mappedTable.getItem(item); + assertThat(result).isEqualTo(item); + } + + @Test + public void putItemTwice_thenGetItem_succeeds() { + Object item1 = firstItem.apply(factory); + mappedTable.putItem(item1); + + Object item2 = secondItem.apply(factory); + mappedTable.putItem(item2); + + long itemCount = mappedTable.scan().items().stream().count(); + assertThat(itemCount).isEqualTo(1L); + + Object result = mappedTable.getItem(item2); + assertThat(result).isEqualTo(item2); + } + + @Test + public void putItemWithResponse_thenGetItemWithResponse_succeeds() { + Object item = fullItem.apply(factory); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)); + GetItemEnhancedResponse getItemEnhancedResponse = + mappedTable.getItemWithResponse(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + + assertThat(putItemEnhancedResponse.attributes()).isNull(); + assertThat(getItemEnhancedResponse.attributes()).isEqualTo(item); + } + + @Test + public void putItem_withCondition_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item); + + Object updated = updatedItem.apply(factory); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "someAttribute") + .putExpressionName("#key1", "stringAttribute") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("stringAttribute-value")) + .build(); + + mappedTable.putItem(PutItemEnhancedRequest.builder(itemClass) + .item(updated) + .conditionExpression(conditionExpression) + .build()); + + Object result = mappedTable.getItem(updated); + assertThat(result).isEqualTo(updated); + } + + @Test + public void putItem_withCondition_throwsConditionalCheckFailedException() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item); + + Object updated = updatedItem.apply(factory); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "someAttribute") + .putExpressionName("#key1", "stringAttribute") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + PutItemEnhancedRequest putItemEnhancedRequest = PutItemEnhancedRequest.builder(itemClass) + .item(updated) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> mappedTable.putItem(putItemEnhancedRequest)) + .isInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void updateItem_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item); + + Object updated = updatedItem.apply(factory); + + Object result = mappedTable.updateItem(updated); + assertThat(result).isEqualTo(updated); + + long itemCount = mappedTable.scan().stream().count(); + assertThat(itemCount).isEqualTo(1L); + } + + @Test + public void updateItem_createsNewCompleteItem_succeeds() { + Object item = fullItem.apply(factory); + + Object result = mappedTable.updateItem(item); + assertThat(result).isEqualTo(item); + } + + @Test + public void updateItem_createsNewPartialItemThenUpdateItem_succeeds() { + Object item = partialItem.apply(factory); + + Object result = mappedTable.updateItem(item); + assertThat(result).isEqualTo(item); + + Object full = fullItem.apply(factory); + + result = mappedTable.updateItem(full); + assertThat(result).isEqualTo(full); + } + + @Test + public void putItem_thenUpdateItemWithNulls_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.updateItem(item); + + Object updatedNullString = updatedItemWithNullString.apply(factory); + + Object result = mappedTable.updateItem(updatedNullString); + assertThat(result).isEqualTo(updatedNullString); + } + + @Test + public void putItem_thenUpdateItemWithIgnoreNulls_succeeds() { + Object item1 = fullItem.apply(factory); + mappedTable.putItem(item1); + + Object item2 = partialItem.apply(factory); + + UpdateItemEnhancedRequest updateItemEnhancedRequest = UpdateItemEnhancedRequest.builder(itemClass) + .item(item2) + .ignoreNulls(true) + .build(); + + Object result = mappedTable.updateItem(updateItemEnhancedRequest); + assertThat(result).isEqualTo(item1); + } + + @Test + public void putItem_thenUpdateItemWithCondition_succeeds() { + Object item = fullItem.apply(factory); + + mappedTable.putItem(item); + + Object updated = updatedItem.apply(factory); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "someAttribute") + .putExpressionName("#key1", "stringAttribute") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("stringAttribute-value")) + .build(); + + UpdateItemEnhancedRequest updateItemEnhancedRequest = UpdateItemEnhancedRequest.builder(itemClass) + .item(updated) + .conditionExpression(conditionExpression) + .build(); + + mappedTable.updateItem(updateItemEnhancedRequest); + + Object result = mappedTable.getItem(updated); + assertThat(result).isEqualTo(updated); + } + + @Test + public void putItem_thenUpdateItemWithCondition_throwsConditionalCheckFailedException() { + Object item = fullItem.apply(factory); + + mappedTable.putItem(item); + + Object updated = updatedItem.apply(factory); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "someAttribute") + .putExpressionName("#key1", "stringAttribute") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + UpdateItemEnhancedRequest updateItemEnhancedRequest = UpdateItemEnhancedRequest.builder(itemClass) + .item(updated) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> mappedTable.updateItem(updateItemEnhancedRequest)) + .isInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndDefaultReturnValue_succeeds() { + Object item = fullItem.apply(factory); + + PutItemEnhancedResponse putItemEnhancedResponse = mappedTable.putItemWithResponse(r -> r.item(item)); + + Object updated = updatedItem.apply(factory); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = mappedTable.updateItemWithResponse(r -> r.item(updated)); + + assertThat(putItemEnhancedResponse.attributes()).isNull(); + assertThat(updateItemEnhancedResponse.attributes()).isEqualTo(updated); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndReturnValueAllOld_succeeds() { + Object item = fullItem.apply(factory); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)); + + Object updated = updatedItem.apply(factory); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + mappedTable.updateItemWithResponse(r -> r.item(updated).returnValues(ReturnValue.ALL_OLD)); + + assertThat(putItemEnhancedResponse.attributes()).isNull(); + assertThat(updateItemEnhancedResponse.attributes()).isEqualTo(item); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndReturnValueNone_succeeds() { + Object item = fullItem.apply(factory); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)); + + Object updated = updatedItem.apply(factory); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + mappedTable.updateItemWithResponse(r -> r.item(updated).returnValues(ReturnValue.NONE)); + + assertThat(putItemEnhancedResponse.attributes()).isNull(); + assertThat(updateItemEnhancedResponse.attributes()).isNull(); + } + + @Test + public void deleteItem_itemNotFound_returnsNullValue() { + Object item = fullItem.apply(factory); + + Object result = mappedTable.deleteItem(item); + assertThat(result).isNull(); + } + + @Test + public void deleteItem_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item); + + Object beforeDeleteResult = mappedTable.deleteItem(item); + Object afterDeleteResult = mappedTable.getItem(item); + + assertThat(beforeDeleteResult).isEqualTo(item); + assertThat(afterDeleteResult).isNull(); + } + + @Test + public void deleteItem_withCondition_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item); + + Object result = mappedTable.getItem(item); + assertThat(result).isEqualTo(item); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", "stringAttribute") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("stringAttribute-value")) + .build(); + + Key key = mappedTable.keyFrom(item); + DeleteItemEnhancedRequest deleteItemEnhancedRequest = DeleteItemEnhancedRequest.builder() + .key(key) + .conditionExpression(conditionExpression) + .build(); + + mappedTable.deleteItem(deleteItemEnhancedRequest); + + result = mappedTable.getItem(item); + assertThat(result).isNull(); + } + + @Test + public void deleteItem_withCondition_throwsConditionalCheckFailedException() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item); + + Object result = mappedTable.getItem(item); + assertThat(result).isEqualTo(item); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", "stringAttribute") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + Key key = mappedTable.keyFrom(item); + + DeleteItemEnhancedRequest deleteItemEnhancedRequest = DeleteItemEnhancedRequest.builder() + .key(key) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> mappedTable.deleteItem(deleteItemEnhancedRequest)) + .isInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void deleteItemWithResponse_succeeds() { + Object item = fullItem.apply(factory); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)); + + Key key = mappedTable.keyFrom(item); + + DeleteItemEnhancedResponse deleteItemEnhancedResponse = + mappedTable.deleteItemWithResponse(r -> r.key(key)); + + assertThat(putItemEnhancedResponse.attributes()).isNull(); + assertThat(deleteItemEnhancedResponse.attributes()).isEqualTo(item); + } + + @Test + public void deleteItemWithResponse_itemNotFound_returnsNullValue() { + Object item = fullItem.apply(factory); + Key key = mappedTable.keyFrom(item); + + DeleteItemEnhancedResponse deleteItemEnhancedResponse = + mappedTable.deleteItemWithResponse(r -> r.key(key)); + + assertThat(deleteItemEnhancedResponse.attributes()).isNull(); + } +} + diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedTableSchemaTestSupport.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedTableSchemaTestSupport.java new file mode 100644 index 000000000000..1a397efa5ff7 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedTableSchemaTestSupport.java @@ -0,0 +1,220 @@ +/* + * 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 java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AbstractBean; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AbstractImmutable; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.SimpleBean; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.SimpleImmutable; + +/** + * Shared parameterized test dimensions and test item builders for + * {@code AnnotatedTableSchemaTest} and {@code AsyncAnnotatedTableSchemaTest}. + */ +final class AnnotatedTableSchemaTestSupport { + + private AnnotatedTableSchemaTestSupport() { + } + + /** + * Widen {@link TableSchema}{@code } to {@code TableSchema} for parameterized tests (unchecked). + */ + @SuppressWarnings("unchecked") + private static TableSchema castTableSchema(Class itemClass) { + return (TableSchema) TableSchema.fromClass(itemClass); + } + + static Collection parameters() { + List rows = Arrays.asList(beanRow(), immutableRow()); + return rows.stream().map(ParameterizedRow::toParameterArray).collect(Collectors.toList()); + } + + private static ParameterizedRow beanRow() { + return new ParameterizedRow( + "@DynamoDbBean", + SimpleBean.class, + castTableSchema(SimpleBean.class), + AbstractBean.class, + TestItemFactory::bean, + TestItemFactory::beanPartial, + TestItemFactory::beanItem1, + TestItemFactory::beanItem2, + TestItemFactory::beanUpdated, + TestItemFactory::beanUpdatedNullString + ); + } + + private static ParameterizedRow immutableRow() { + return new ParameterizedRow( + "@DynamoDbImmutable", + SimpleImmutable.class, + castTableSchema(SimpleImmutable.class), + AbstractImmutable.class, + TestItemFactory::immutable, + TestItemFactory::immutablePartial, + TestItemFactory::immutableItem1, + TestItemFactory::immutableItem2, + TestItemFactory::immutableUpdated, + TestItemFactory::immutableUpdatedNullString + ); + } + + /** + * One {@link org.junit.runners.Parameterized} data row; named fields replace positional {@code Object[]} indexing here only. + */ + private static final class ParameterizedRow { + private final String schemaType; + private final Class itemClass; + private final TableSchema tableSchema; + private final Class abstractItemClass; + private final Function fullItem; + private final Function partialItem; + private final Function firstItem; + private final Function secondItem; + private final Function updatedItem; + private final Function updatedItemWithNullString; + + private ParameterizedRow( + String schemaType, + Class itemClass, + TableSchema tableSchema, + Class abstractItemClass, + Function fullItem, + Function partialItem, + Function firstItem, + Function secondItem, + Function updatedItem, + Function updatedItemWithNullString) { + + this.schemaType = schemaType; + this.itemClass = itemClass; + this.tableSchema = tableSchema; + this.abstractItemClass = abstractItemClass; + this.fullItem = fullItem; + this.partialItem = partialItem; + this.firstItem = firstItem; + this.secondItem = secondItem; + this.updatedItem = updatedItem; + this.updatedItemWithNullString = updatedItemWithNullString; + } + + @SuppressWarnings("unchecked") + private Object[] toParameterArray() { + return new Object[] { + schemaType, + (Class) itemClass, + tableSchema, + fullItem, + partialItem, + firstItem, + secondItem, + updatedItem, + updatedItemWithNullString, + abstractItemClass + }; + } + } + + static final class TestItemFactory { + + private static final String DEFAULT_ID = "id-value"; + private static final String DEFAULT_SORT = "sort-value"; + + private static final String ATTR_FULL = "stringAttribute-value"; + private static final String ATTR_ITEM1 = "stringAttribute-value-item1"; + private static final String ATTR_ITEM2 = "stringAttribute-value-item2"; + private static final String ATTR_UPDATED = "stringAttribute-value-updated"; + + private final String id = DEFAULT_ID; + private final String sort = DEFAULT_SORT; + + Object bean() { + return beanWithStringAttribute(ATTR_FULL); + } + + Object beanPartial() { + SimpleBean item = new SimpleBean(); + item.setId(id); + item.setSort(sort); + return item; + } + + Object beanItem1() { + return beanWithStringAttribute(ATTR_ITEM1); + } + + Object beanItem2() { + return beanWithStringAttribute(ATTR_ITEM2); + } + + Object beanUpdated() { + return beanWithStringAttribute(ATTR_UPDATED); + } + + Object beanUpdatedNullString() { + return beanWithStringAttribute(null); + } + + Object immutable() { + return immutableWithStringAttribute(ATTR_FULL); + } + + Object immutablePartial() { + return SimpleImmutable.builder() + .id(id) + .sort(sort) + .build(); + } + + Object immutableItem1() { + return immutableWithStringAttribute(ATTR_ITEM1); + } + + Object immutableItem2() { + return immutableWithStringAttribute(ATTR_ITEM2); + } + + Object immutableUpdated() { + return immutableWithStringAttribute(ATTR_UPDATED); + } + + Object immutableUpdatedNullString() { + return immutableWithStringAttribute(null); + } + + private SimpleBean beanWithStringAttribute(String stringAttribute) { + SimpleBean item = new SimpleBean(); + item.setId(id); + item.setSort(sort); + item.setStringAttribute(stringAttribute); + return item; + } + + private SimpleImmutable immutableWithStringAttribute(String stringAttribute) { + return SimpleImmutable.builder() + .id(id) + .sort(sort) + .stringAttribute(stringAttribute) + .build(); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAnnotatedTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAnnotatedTableSchemaTest.java new file mode 100644 index 000000000000..95448c0c2807 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAnnotatedTableSchemaTest.java @@ -0,0 +1,595 @@ +/* + * 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.concurrent.CompletionException; +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.core.async.SdkPublisher; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +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 AsyncAnnotatedTableSchemaTest extends LocalDynamoDbAsyncTestBase { + + private static final String TABLE_NAME = "table-name"; + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + return AnnotatedTableSchemaTestSupport.parameters(); + } + + @Parameterized.Parameter(0) + public String schemaType; + + @Parameterized.Parameter(1) + public Class itemClass; + + @Parameterized.Parameter(2) + public TableSchema tableSchema; + + @Parameterized.Parameter(3) + public Function fullItem; + + @Parameterized.Parameter(4) + public Function partialItem; + + @Parameterized.Parameter(5) + public Function firstItem; + + @Parameterized.Parameter(6) + public Function secondItem; + + @Parameterized.Parameter(7) + public Function updatedItem; + + @Parameterized.Parameter(8) + public Function updatedItemWithNullString; + + @Parameterized.Parameter(9) + public Class abstractItemClass; + + private final DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .build(); + + private DynamoDbAsyncTable mappedTable; + private AnnotatedTableSchemaTestSupport.TestItemFactory factory; + + @Before + public void createTable() { + mappedTable = enhancedClient.table(getConcreteTableName(TABLE_NAME), tableSchema); + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableExists(b -> b.tableName(getConcreteTableName(TABLE_NAME))).join(); + + factory = new AnnotatedTableSchemaTestSupport.TestItemFactory(); + } + + @After + public void deleteTable() { + try { + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(TABLE_NAME)) + .build()).join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableNotExists(b -> b.tableName(getConcreteTableName(TABLE_NAME))).join(); + } catch (ResourceNotFoundException ignored) { + // Table doesn't exist, nothing to delete + } + } + + @Test + public void describeTable_succeeds() { + DescribeTableEnhancedResponse describeTableEnhancedResponse = mappedTable.describeTable().join(); + Assertions.assertThat(describeTableEnhancedResponse.table()).isNotNull(); + Assertions.assertThat(describeTableEnhancedResponse.table().tableName()) + .isEqualTo(getConcreteTableName(TABLE_NAME)); + } + + @Test + public void createTableWithDefaults_thenDeleteTable_succeeds() { + String tableName = TABLE_NAME + "-1"; + + DynamoDbAsyncTable anotherMappedTable = enhancedClient.table(getConcreteTableName(tableName), tableSchema); + anotherMappedTable.createTable().join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableExists(b -> b.tableName(getConcreteTableName(tableName))).join(); + + DescribeTableEnhancedResponse describeTableEnhancedResponse = anotherMappedTable.describeTable().join(); + TableDescription tableDescription = describeTableEnhancedResponse.table(); + + String actualTableName = tableDescription.tableName(); + Long actualReadCapacityUnits = tableDescription.provisionedThroughput().readCapacityUnits(); + Long actualWriteCapacityUnits = tableDescription.provisionedThroughput().writeCapacityUnits(); + + assertThat(actualTableName).isEqualTo(getConcreteTableName(tableName)); + assertThat(actualReadCapacityUnits).isEqualTo(0L); + assertThat(actualWriteCapacityUnits).isEqualTo(0L); + + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(tableName)) + .build()).join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableNotExists(b -> b.tableName(getConcreteTableName(tableName))).join(); + + assertThatThrownBy(() -> anotherMappedTable.describeTable().join()) + .isInstanceOf(CompletionException.class) + .hasCauseInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("Cannot do operations on a non-existent table"); + } + + @Test + public void createTableWithProvisionedThroughput_succeeds() { + String tableName = TABLE_NAME + "-1"; + + DynamoDbAsyncTable anotherMappedTable = enhancedClient.table(getConcreteTableName(tableName), tableSchema); + anotherMappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableExists(b -> b.tableName(getConcreteTableName(tableName))).join(); + + DescribeTableEnhancedResponse describeTableEnhancedResponse = anotherMappedTable.describeTable().join(); + TableDescription tableDescription = describeTableEnhancedResponse.table(); + + String actualTableName = tableDescription.tableName(); + Long actualReadCapacityUnits = tableDescription.provisionedThroughput().readCapacityUnits(); + Long actualWriteCapacityUnits = tableDescription.provisionedThroughput().writeCapacityUnits(); + + assertThat(actualTableName).isEqualTo(getConcreteTableName(tableName)); + assertThat(actualReadCapacityUnits).isEqualTo(getDefaultProvisionedThroughput().readCapacityUnits()); + assertThat(actualWriteCapacityUnits).isEqualTo(getDefaultProvisionedThroughput().writeCapacityUnits()); + + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(tableName)) + .build()).join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableNotExists(b -> b.tableName(getConcreteTableName(tableName))).join(); + + assertThatThrownBy(() -> anotherMappedTable.describeTable().join()) + .isInstanceOf(CompletionException.class) + .hasRootCauseInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("Cannot do operations on a non-existent table"); + } + + @Test + public void createTableWithDefaults_throwsIllegalArgumentException() { + TableSchema badSchema = TableSchema.fromClass(abstractItemClass); + DynamoDbAsyncTable other = enhancedClient.table(getConcreteTableName(TABLE_NAME), badSchema); + + assertThatThrownBy(() -> other.createTable().join()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Attempt to execute an operation that requires a primary index without defining any primary" + + " key attributes in the table metadata."); + } + + @Test + public void createTableWithProvisionedThroughput_throwsIllegalArgumentException() { + TableSchema badSchema = TableSchema.fromClass(abstractItemClass); + DynamoDbAsyncTable other = enhancedClient.table(getConcreteTableName(TABLE_NAME), badSchema); + + assertThatThrownBy(() -> other.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Attempt to execute an operation that requires a primary index without defining any primary" + + " key attributes in the table metadata."); + } + + @Test + public void getItem_itemNotFound_returnsNullValue() { + Object item = fullItem.apply(factory); + + Object result = mappedTable.getItem(item).join(); + assertThat(result).isNull(); + } + + @Test + public void getItemWithResponse_itemNotFound_returnsNullValue() { + GetItemEnhancedResponse getItemEnhancedResponse = + mappedTable.getItemWithResponse(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + + assertThat(getItemEnhancedResponse.attributes()).isNull(); + assertThat(getItemEnhancedResponse.consumedCapacity()).isNull(); + } + + @Test + public void putItem_thenGetItem_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item).join(); + + Object result = mappedTable.getItem(item).join(); + assertThat(result).isEqualTo(item); + } + + @Test + public void putItemPartial_thenGetItem_succeeds() { + Object item = partialItem.apply(factory); + mappedTable.putItem(item).join(); + + Object result = mappedTable.getItem(item).join(); + assertThat(result).isEqualTo(item); + } + + @Test + public void putItemTwice_thenGetItem_succeeds() { + Object item1 = firstItem.apply(factory); + mappedTable.putItem(item1).join(); + + Object item2 = secondItem.apply(factory); + mappedTable.putItem(item2).join(); + + SdkPublisher publisher = mappedTable.scan().items(); + drainPublisher(publisher, 1); + + Object result = mappedTable.getItem(item2).join(); + assertThat(result).isEqualTo(item2); + } + + @Test + public void putItemWithResponse_thenGetItemWithResponse_succeeds() { + Object item = fullItem.apply(factory); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)).join(); + GetItemEnhancedResponse getItemEnhancedResponse = + mappedTable.getItemWithResponse(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + + assertThat(putItemEnhancedResponse.attributes()).isNull(); + assertThat(getItemEnhancedResponse.attributes()).isEqualTo(item); + } + + @Test + public void putItem_withCondition_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item).join(); + + Object updated = updatedItem.apply(factory); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "someAttribute") + .putExpressionName("#key1", "stringAttribute") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("stringAttribute-value")) + .build(); + + mappedTable.putItem(PutItemEnhancedRequest.builder(itemClass) + .item(updated) + .conditionExpression(conditionExpression) + .build()).join(); + + Object result = mappedTable.getItem(updated).join(); + assertThat(result).isEqualTo(updated); + } + + @Test + public void putItem_withCondition_throwsConditionalCheckFailedException() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item).join(); + + Object updated = updatedItem.apply(factory); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "someAttribute") + .putExpressionName("#key1", "stringAttribute") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + PutItemEnhancedRequest putItemEnhancedRequest = PutItemEnhancedRequest.builder(itemClass) + .item(updated) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> mappedTable.putItem(putItemEnhancedRequest).join()) + .isInstanceOf(CompletionException.class) + .hasRootCauseInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void updateItem_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item).join(); + + Object updated = updatedItem.apply(factory); + + Object result = mappedTable.updateItem(updated).join(); + assertThat(result).isEqualTo(updated); + + SdkPublisher publisher = mappedTable.scan().items(); + drainPublisher(publisher, 1); + } + + @Test + public void updateItem_createsNewCompleteItem_succeeds() { + Object item = fullItem.apply(factory); + + Object result = mappedTable.updateItem(item).join(); + assertThat(result).isEqualTo(item); + } + + @Test + public void updateItem_createsNewPartialItemThenUpdateItem_succeeds() { + Object item = partialItem.apply(factory); + + Object result = mappedTable.updateItem(item).join(); + assertThat(result).isEqualTo(item); + + Object full = fullItem.apply(factory); + + result = mappedTable.updateItem(full).join(); + assertThat(result).isEqualTo(full); + } + + @Test + public void putItem_thenUpdateItemWithNulls_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.updateItem(item).join(); + + Object updatedNullString = updatedItemWithNullString.apply(factory); + + Object result = mappedTable.updateItem(updatedNullString).join(); + assertThat(result).isEqualTo(updatedNullString); + } + + @Test + public void putItem_thenUpdateItemWithIgnoreNulls_succeeds() { + Object item1 = fullItem.apply(factory); + mappedTable.putItem(item1).join(); + + Object partial = partialItem.apply(factory); + + UpdateItemEnhancedRequest updateItemEnhancedRequest = UpdateItemEnhancedRequest.builder(itemClass) + .item(partial) + .ignoreNulls(true) + .build(); + + Object result = mappedTable.updateItem(updateItemEnhancedRequest).join(); + assertThat(result).isEqualTo(item1); + } + + @Test + public void putItem_thenUpdateItemWithCondition_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item).join(); + + Object updated = updatedItem.apply(factory); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "someAttribute") + .putExpressionName("#key1", "stringAttribute") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("stringAttribute-value")) + .build(); + + UpdateItemEnhancedRequest updateItemEnhancedRequest = UpdateItemEnhancedRequest.builder(itemClass) + .item(updated) + .conditionExpression(conditionExpression) + .build(); + + mappedTable.updateItem(updateItemEnhancedRequest).join(); + + Object result = mappedTable.getItem(updated).join(); + assertThat(result).isEqualTo(updated); + } + + @Test + public void putItem_thenUpdateItemWithCondition_throwsConditionalCheckFailedException() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item).join(); + + Object updated = updatedItem.apply(factory); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "someAttribute") + .putExpressionName("#key1", "stringAttribute") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + UpdateItemEnhancedRequest updateItemEnhancedRequest = UpdateItemEnhancedRequest.builder(itemClass) + .item(updated) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> mappedTable.updateItem(updateItemEnhancedRequest).join()) + .isInstanceOf(CompletionException.class) + .hasRootCauseInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndDefaultReturnValue_succeeds() { + Object item = fullItem.apply(factory); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)).join(); + + Object updated = updatedItem.apply(factory); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + mappedTable.updateItemWithResponse(r -> r.item(updated)).join(); + + assertThat(putItemEnhancedResponse.attributes()).isNull(); + assertThat(updateItemEnhancedResponse.attributes()).isEqualTo(updated); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndReturnValueAllOld_succeeds() { + Object item = fullItem.apply(factory); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)).join(); + + Object updated = updatedItem.apply(factory); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + mappedTable.updateItemWithResponse(r -> r.item(updated).returnValues(ReturnValue.ALL_OLD)).join(); + + assertThat(putItemEnhancedResponse.attributes()).isNull(); + assertThat(updateItemEnhancedResponse.attributes()).isEqualTo(item); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndReturnValueNone_succeeds() { + Object item = fullItem.apply(factory); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)).join(); + + Object updated = updatedItem.apply(factory); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + mappedTable.updateItemWithResponse(r -> r.item(updated).returnValues(ReturnValue.NONE)).join(); + + assertThat(putItemEnhancedResponse.attributes()).isNull(); + assertThat(updateItemEnhancedResponse.attributes()).isNull(); + } + + @Test + public void deleteItem_itemNotFound_returnsNullValue() { + Object item = fullItem.apply(factory); + + Object result = mappedTable.deleteItem(item).join(); + assertThat(result).isNull(); + } + + @Test + public void deleteItem_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item).join(); + + Object beforeDeleteResult = mappedTable.deleteItem(item).join(); + Object afterDeleteResult = mappedTable.getItem(item).join(); + + assertThat(beforeDeleteResult).isEqualTo(item); + assertThat(afterDeleteResult).isNull(); + } + + @Test + public void deleteItem_withCondition_succeeds() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item).join(); + + Object result = mappedTable.getItem(item).join(); + assertThat(result).isEqualTo(item); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", "stringAttribute") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("stringAttribute-value")) + .build(); + + Key key = mappedTable.keyFrom(item); + DeleteItemEnhancedRequest deleteItemEnhancedRequest = DeleteItemEnhancedRequest.builder() + .key(key) + .conditionExpression(conditionExpression) + .build(); + + mappedTable.deleteItem(deleteItemEnhancedRequest).join(); + + result = mappedTable.getItem(item).join(); + assertThat(result).isNull(); + } + + @Test + public void deleteItem_withCondition_throwsConditionalCheckFailedException() { + Object item = fullItem.apply(factory); + mappedTable.putItem(item).join(); + + Object result = mappedTable.getItem(item).join(); + assertThat(result).isEqualTo(item); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", "stringAttribute") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + Key key = mappedTable.keyFrom(item); + + DeleteItemEnhancedRequest deleteItemEnhancedRequest = DeleteItemEnhancedRequest.builder() + .key(key) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> mappedTable.deleteItem(deleteItemEnhancedRequest).join()) + .isInstanceOf(CompletionException.class) + .hasRootCauseInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void deleteItemWithResponse_succeeds() { + Object item = fullItem.apply(factory); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)).join(); + + Key key = mappedTable.keyFrom(item); + + DeleteItemEnhancedResponse deleteItemEnhancedResponse = + mappedTable.deleteItemWithResponse(r -> r.key(key)).join(); + + assertThat(putItemEnhancedResponse.attributes()).isNull(); + assertThat(deleteItemEnhancedResponse.attributes()).isEqualTo(item); + } + + @Test + public void deleteItemWithResponse_itemNotFound_returnsNullValue() { + Object item = fullItem.apply(factory); + Key key = mappedTable.keyFrom(item); + + DeleteItemEnhancedResponse deleteItemEnhancedResponse = + mappedTable.deleteItemWithResponse(r -> r.key(key)).join(); + + assertThat(deleteItemEnhancedResponse.attributes()).isNull(); + } +} + diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AtomicCounterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AtomicCounterTest.java index 9b3d12e6d55f..b65983937bfd 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AtomicCounterTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AtomicCounterTest.java @@ -16,18 +16,21 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.List; import java.util.stream.IntStream; +import org.apache.logging.log4j.core.LogEvent; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.slf4j.event.Level; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.LogCaptor; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AtomicCounterExtension; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AtomicCounterRecord; import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; -import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; public class AtomicCounterTest extends LocalDynamoDbSyncTestBase { @@ -194,4 +197,23 @@ public void batchPut_initializesCorrectly() { assertThat(persistedRecord.getAttribute1()).isEqualTo(STRING_VALUE); assertThat(persistedRecord.getDefaultCounter()).isEqualTo(1L); } + + @Test + public void updateItem_withAtomicCounters_logsFilteredAttributes() { + AtomicCounterRecord record = new AtomicCounterRecord(); + record.setId(RECORD_ID); + record.setAttribute1(STRING_VALUE); + + try (LogCaptor logCaptor = new LogCaptor(AtomicCounterExtension.class, Level.DEBUG)) { + mappedTable.updateItem(record); + + 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("Filtered atomic counter attributes from " + + "existing update item to avoid " + + "collisions: customCounter," + + "defaultCounter,decreasingCounter"); + } + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AtomicCounterExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AtomicCounterExtensionTest.java new file mode 100644 index 000000000000..db2f17f9c463 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AtomicCounterExtensionTest.java @@ -0,0 +1,331 @@ +/* + * 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.extensions; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.atomicCounter; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; + +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAtomicCounter; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; +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; + +public class AtomicCounterExtensionTest extends LocalDynamoDbSyncTestBase { + + private static final StaticTableSchema TABLE_SCHEMA = + StaticTableSchema.builder(StaticCounterRecord.class) + .newItemSupplier(StaticCounterRecord::new) + .addAttribute(String.class, + a -> a.name("id1") + .getter(StaticCounterRecord::getId) + .setter(StaticCounterRecord::setId) + .addTag(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("data") + .getter(StaticCounterRecord::getData) + .setter(StaticCounterRecord::setData)) + .addAttribute(Long.class, + a -> a.name("defaultCounter") + .getter(StaticCounterRecord::getDefaultCounter) + .setter(StaticCounterRecord::setDefaultCounter) + .addTag(atomicCounter())) + .addAttribute(Long.class, + a -> a.name("customCounter") + .getter(StaticCounterRecord::getCustomCounter) + .setter(StaticCounterRecord::setCustomCounter) + .addTag(atomicCounter(5, 10))) + .build(); + + private final DynamoDbEnhancedClient enhancedClient = + DynamoDbEnhancedClient.builder().dynamoDbClient(getDynamoDbClient()).build(); + + private final DynamoDbTable beanMappedTable = enhancedClient.table( + getConcreteTableName("atomic-counter-table-bean"), BeanTableSchema.create(BeanCounterRecord.class)); + + private final DynamoDbTable staticMappedTable = enhancedClient.table( + getConcreteTableName("atomic-counter-table-static"), TABLE_SCHEMA); + + @Before + public void createTable() { + staticMappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + beanMappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(r -> r.tableName( + getConcreteTableName("atomic-counter-table-bean"))); + getDynamoDbClient().deleteTable(r -> r.tableName( + getConcreteTableName("atomic-counter-table-static"))); + } + + @Test + public void putItem_beanSchema_initializesCountersWithDefaultValues() { + BeanCounterRecord beanRecord = new BeanCounterRecord(); + beanRecord.setId("id"); + + beanMappedTable.putItem(beanRecord); + + BeanCounterRecord retrieved = beanMappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + assertThat(retrieved).isNotNull(); + assertThat(retrieved.getId()).isEqualTo("id"); + assertThat(retrieved.getDefaultCounter()).isEqualTo(0L); + assertThat(retrieved.getCustomCounter()).isEqualTo(10L); + } + + @Test + public void updateItem_beanSchema_incrementsCounters() { + BeanCounterRecord beanRecord = new BeanCounterRecord(); + beanRecord.setId("id1"); + beanRecord.setData("data1"); + beanMappedTable.putItem(beanRecord); + + BeanCounterRecord update = new BeanCounterRecord(); + update.setId("id1"); + update.setData("data2"); + beanMappedTable.updateItem(update); + + BeanCounterRecord retrieved = beanMappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))); + assertThat(retrieved).isNotNull(); + assertThat(retrieved.getData()).isEqualTo("data2"); + assertThat(retrieved.getDefaultCounter()).isEqualTo(1L); + assertThat(retrieved.getCustomCounter()).isEqualTo(15L); + } + + @Test + public void updateItem_beanSchema_multipleUpdates_incrementsCountersCorrectly() { + BeanCounterRecord record = new BeanCounterRecord(); + record.setId("id1"); + record.setData("data1"); + beanMappedTable.putItem(record); + + for (int i = 2; i <= 10; i++) { + BeanCounterRecord update = new BeanCounterRecord(); + update.setId("id1"); + update.setData(String.format("data%d", i)); + beanMappedTable.updateItem(update); + } + + BeanCounterRecord retrieved = beanMappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))); + assertThat(retrieved).isNotNull(); + assertThat(retrieved.getData()).isEqualTo("data10"); + assertThat(retrieved.getDefaultCounter()).isEqualTo(9L); + assertThat(retrieved.getCustomCounter()).isEqualTo(55L); + } + + @Test + public void putItem_beanSchema_withExistingCounterValues_overwritesWithStartValues() { + BeanCounterRecord record = new BeanCounterRecord(); + record.setId("id1"); + record.setDefaultCounter(100L); + record.setCustomCounter(200L); + + beanMappedTable.putItem(record); + + BeanCounterRecord retrieved = beanMappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))); + assertThat(retrieved).isNotNull(); + assertThat(retrieved.getDefaultCounter()).isEqualTo(0L); + assertThat(retrieved.getCustomCounter()).isEqualTo(10L); + } + + @Test + public void putItem_staticSchema_initializesCountersWithDefaultValues() { + StaticCounterRecord record = new StaticCounterRecord(); + record.setId("id1"); + + staticMappedTable.putItem(record); + + StaticCounterRecord retrieved = staticMappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))); + assertThat(retrieved).isNotNull(); + assertThat(retrieved.getId()).isEqualTo("id1"); + assertThat(retrieved.getDefaultCounter()).isEqualTo(0L); + assertThat(retrieved.getCustomCounter()).isEqualTo(10L); + } + + @Test + public void updateItem_staticSchema_incrementsCounters() { + StaticCounterRecord record = new StaticCounterRecord(); + record.setId("id1"); + record.setData("data1"); + staticMappedTable.putItem(record); + + StaticCounterRecord update = new StaticCounterRecord(); + update.setId("id1"); + update.setData("data2"); + staticMappedTable.updateItem(update); + + StaticCounterRecord retrieved = staticMappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))); + assertThat(retrieved).isNotNull(); + assertThat(retrieved.getData()).isEqualTo("data2"); + assertThat(retrieved.getDefaultCounter()).isEqualTo(1L); + assertThat(retrieved.getCustomCounter()).isEqualTo(15L); + } + + @Test + public void updateItem_staticSchema_multipleUpdates_incrementsCountersCorrectly() { + StaticCounterRecord record = new StaticCounterRecord(); + record.setId("id1"); + record.setData("data1"); + staticMappedTable.putItem(record); + + for (int i = 2; i <= 10; i++) { + StaticCounterRecord update = new StaticCounterRecord(); + update.setId("id1"); + update.setData(String.format("data%d", i)); + staticMappedTable.updateItem(update); + } + + StaticCounterRecord retrieved = staticMappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))); + assertThat(retrieved).isNotNull(); + assertThat(retrieved.getData()).isEqualTo("data10"); + assertThat(retrieved.getDefaultCounter()).isEqualTo(9L); + assertThat(retrieved.getCustomCounter()).isEqualTo(55L); + } + + @Test + public void putItem_staticSchema_withExistingCounterValues_overwritesWithStartValues() { + StaticCounterRecord record = new StaticCounterRecord(); + record.setId("id1"); + record.setDefaultCounter(100L); + record.setCustomCounter(200L); + + staticMappedTable.putItem(record); + + StaticCounterRecord retrieved = staticMappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))); + assertThat(retrieved).isNotNull(); + assertThat(retrieved.getDefaultCounter()).isEqualTo(0L); + assertThat(retrieved.getCustomCounter()).isEqualTo(10L); + } + + @DynamoDbBean + public static class BeanCounterRecord { + private String id; + private String data; + private Long defaultCounter; + private Long customCounter; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + @DynamoDbAtomicCounter + public Long getDefaultCounter() { + return defaultCounter; + } + + public void setDefaultCounter(Long defaultCounter) { + this.defaultCounter = defaultCounter; + } + + @DynamoDbAtomicCounter(delta = 5, startValue = 10) + public Long getCustomCounter() { + return customCounter; + } + + public void setCustomCounter(Long customCounter) { + this.customCounter = customCounter; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + BeanCounterRecord that = (BeanCounterRecord) o; + return Objects.equals(id, that.id) && Objects.equals(data, that.data) && Objects.equals(defaultCounter, that.defaultCounter) && Objects.equals(customCounter, that.customCounter); + } + + @Override + public int hashCode() { + return Objects.hash(id, data, defaultCounter, customCounter); + } + } + + public static class StaticCounterRecord { + private String id; + private String data; + private Long defaultCounter; + private Long customCounter; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public Long getDefaultCounter() { + return defaultCounter; + } + + public void setDefaultCounter(Long defaultCounter) { + this.defaultCounter = defaultCounter; + } + + public Long getCustomCounter() { + return customCounter; + } + + public void setCustomCounter(Long customCounter) { + this.customCounter = customCounter; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + StaticCounterRecord that = (StaticCounterRecord) o; + return Objects.equals(id, that.id) && Objects.equals(data, that.data) && Objects.equals(defaultCounter, that.defaultCounter) && Objects.equals(customCounter, that.customCounter); + } + + @Override + public int hashCode() { + return Objects.hash(id, data, defaultCounter, customCounter); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/ChainExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/ChainExtensionTest.java new file mode 100644 index 000000000000..26b7af27c06e --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/ChainExtensionTest.java @@ -0,0 +1,390 @@ +/* + * 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.extensions; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.AutoGeneratedUuidRecordTest.assertValidUuid; + +import java.time.Instant; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.After; +import org.junit.Before; +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.extensions.AutoGeneratedTimestampRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AtomicCounterExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAtomicCounter; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver; +import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; + +public class ChainExtensionTest extends LocalDynamoDbSyncTestBase { + + private static final String TABLE_NAME = "table-name"; + private static final String ITEM_ID = "test-id"; + private static final String ITEM_DATA_CREATED = "created"; + private static final String ITEM_DATA_UPDATED = "updated"; + + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(MultiExtensionRecord.class); + + private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient + .builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(Stream.concat( + ExtensionResolver.defaultExtensions().stream(), + Stream.of( + AutoGeneratedUuidExtension.create(), + AutoGeneratedTimestampRecordExtension.create())) + .collect(Collectors.toList())) + .build(); + + private final DynamoDbTable mappedTable = + enhancedClient.table(getConcreteTableName(TABLE_NAME), TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(r -> r.tableName(getConcreteTableName(TABLE_NAME))); + } + + @Test + public void transactWriteItem_putItemThenUpdateItemAndGetItem_appliesAllChainedExtensions() { + MultiExtensionRecord record = new MultiExtensionRecord(); + record.setId(ITEM_ID); + record.setData(ITEM_DATA_CREATED); + + Instant beforePut = Instant.now(); + enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + Instant afterPut = Instant.now(); + + MultiExtensionRecord resultAfterPut = scanTableAndFindFirstItem(); + + assertRecord(resultAfterPut, + ITEM_DATA_CREATED, + beforePut, + afterPut, + beforePut, + afterPut, + 0L, + 10L, + 1L); + + resultAfterPut.setData(ITEM_DATA_UPDATED); + + Instant beforeUpdate = Instant.now(); + enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() + .addUpdateItem(mappedTable, resultAfterPut) + .build()); + Instant afterUpdate = Instant.now(); + + MultiExtensionRecord resultAfterUpdate = scanTableAndFindFirstItem(); + + assertRecord(resultAfterUpdate, + ITEM_DATA_UPDATED, + beforePut, + afterPut, + beforeUpdate, + afterUpdate, + 1L, + 15L, + 2L); + } + + @Test + public void putItemThenUpdateItemAndGetItem_appliesAllChainedExtensions() { + MultiExtensionRecord record = new MultiExtensionRecord(); + record.setId(ITEM_ID); + record.setData(ITEM_DATA_CREATED); + + Instant beforePut = Instant.now(); + mappedTable.putItem(record); + Instant afterPut = Instant.now(); + + MultiExtensionRecord resultAfterPut = scanTableAndFindFirstItem(); + + assertRecord(resultAfterPut, + ITEM_DATA_CREATED, + beforePut, + afterPut, + beforePut, + afterPut, + 0L, + 10L, + 1L); + + resultAfterPut.setData(ITEM_DATA_UPDATED); + Instant beforeUpdate = Instant.now(); + mappedTable.updateItem(resultAfterPut); + Instant afterUpdate = Instant.now(); + + MultiExtensionRecord resultAfterUpdate = scanTableAndFindFirstItem(); + + assertRecord(resultAfterUpdate, + ITEM_DATA_UPDATED, + beforePut, + afterPut, + beforeUpdate, + afterUpdate, + 1L, + 15L, + 2L); + } + + @Test + public void batchWriteItem_putTwoItems_appliesAllChainedExtensionsWithoutVersion() { + DynamoDbEnhancedClient enhancedClientWithoutVersionExtension = DynamoDbEnhancedClient + .builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions( + AtomicCounterExtension.builder().build(), + AutoGeneratedUuidExtension.create(), + AutoGeneratedTimestampRecordExtension.create()) + .build(); + + DynamoDbTable mappedTableWithoutVersionExtension = + enhancedClientWithoutVersionExtension.table(getConcreteTableName(TABLE_NAME), TABLE_SCHEMA); + + MultiExtensionRecord record1 = new MultiExtensionRecord(); + record1.setId(ITEM_ID + "-1"); + record1.setData(ITEM_DATA_CREATED); + + MultiExtensionRecord record2 = new MultiExtensionRecord(); + record2.setId(ITEM_ID + "-2"); + record2.setData(ITEM_DATA_CREATED); + + Instant beforePut = Instant.now(); + enhancedClientWithoutVersionExtension.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(MultiExtensionRecord.class) + .mappedTableResource(mappedTableWithoutVersionExtension) + .addPutItem(record1) + .addPutItem(record2) + .build())); + Instant afterPut = Instant.now(); + + List results = mappedTableWithoutVersionExtension.scan().items().stream().collect(Collectors.toList()); + assertThat(results).hasSize(2); + + results.forEach(r -> { + assertRecord(r, + ITEM_DATA_CREATED, + beforePut, + afterPut, + beforePut, + afterPut, + 0L, + 10L, + null); + }); + } + + @Test + public void batchWriteItem_putTwoItemsAndAppliesAllChainedExtensions_throwsIllegalArgumentException() { + MultiExtensionRecord record1 = new MultiExtensionRecord(); + record1.setId(ITEM_ID + "-1"); + record1.setData(ITEM_DATA_CREATED); + + MultiExtensionRecord record2 = new MultiExtensionRecord(); + record2.setId(ITEM_ID + "-2"); + record2.setData(ITEM_DATA_CREATED); + + assertThatThrownBy(() -> enhancedClient.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(MultiExtensionRecord.class) + .mappedTableResource(mappedTable) + .addPutItem(record1) + .addPutItem(record2) + .build()))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("A mapper extension inserted a conditionExpression in a PutItem request as part of a " + + "BatchWriteItemRequest. This is not supported by DynamoDb. An extension known to do " + + "this is the VersionedRecordExtension which is loaded by default unless overridden. " + + "To fix this use a table schema that does not have a versioned attribute in it or do " + + "not load the offending extension."); + } + + private void assertRecord(MultiExtensionRecord result, + String expectedData, + Instant expectedCreatedAtNotBefore, + Instant expectedCreatedAtNotAfter, + Instant expectedUpdatedAtNotBefore, + Instant expectedUpdatedAtNotAfter, + Long expectedDefaultCounter, + Long expectedCustomCounter, + Long expectedVersion) { + assertThat(result).isNotNull(); + assertThat(result.getData()).isEqualTo(expectedData); + + assertValidUuid(result.getAutogeneratedUuidWriteIfNotExists()); + assertValidUuid(result.getAutogeneratedUuidWriteAlways()); + + assertThat(result.getCreatedAt()).isNotNull(); + assertThat(result.getCreatedAt()).isAfter(expectedCreatedAtNotBefore); + assertThat(result.getCreatedAt()).isBefore(expectedCreatedAtNotAfter); + + assertThat(result.getUpdatedAt()).isNotNull(); + assertThat(result.getUpdatedAt()).isAfter(expectedUpdatedAtNotBefore); + assertThat(result.getUpdatedAt()).isBefore(expectedUpdatedAtNotAfter); + + assertThat(result.getDefaultCounter()).isEqualTo(expectedDefaultCounter); + assertThat(result.getCustomCounter()).isEqualTo(expectedCustomCounter); + + assertThat(result.getVersion()).isEqualTo(expectedVersion); + } + + private MultiExtensionRecord scanTableAndFindFirstItem() { + return mappedTable.scan().items().stream().findFirst().orElse(null); + } + + @DynamoDbBean + public static class MultiExtensionRecord { + private String id; + private String autogeneratedUuidWriteIfNotExists; + private String autogeneratedUuidWriteAlways; + private String data; + private Instant createdAt; + private Instant updatedAt; + private Long defaultCounter; + private Long customCounter; + private Long version; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbAutoGeneratedUuid + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAutogeneratedUuidWriteIfNotExists() { + return autogeneratedUuidWriteIfNotExists; + } + + public void setAutogeneratedUuidWriteIfNotExists(String autogeneratedUuidWriteIfNotExists) { + this.autogeneratedUuidWriteIfNotExists = autogeneratedUuidWriteIfNotExists; + } + + @DynamoDbAutoGeneratedUuid + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + public String getAutogeneratedUuidWriteAlways() { + return autogeneratedUuidWriteAlways; + } + + public void setAutogeneratedUuidWriteAlways(String autogeneratedUuidWriteAlways) { + this.autogeneratedUuidWriteAlways = autogeneratedUuidWriteAlways; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + @DynamoDbAutoGeneratedTimestampAttribute + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + @DynamoDbAutoGeneratedTimestampAttribute + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + @DynamoDbAtomicCounter + public Long getDefaultCounter() { + return defaultCounter; + } + + public void setDefaultCounter(Long defaultCounter) { + this.defaultCounter = defaultCounter; + } + + @DynamoDbAtomicCounter(delta = 5, startValue = 10) + public Long getCustomCounter() { + return customCounter; + } + + public void setCustomCounter(Long customCounter) { + this.customCounter = customCounter; + } + + @DynamoDbVersionAttribute + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + MultiExtensionRecord that = (MultiExtensionRecord) o; + return Objects.equals(id, that.id) + && Objects.equals(autogeneratedUuidWriteIfNotExists, that.autogeneratedUuidWriteIfNotExists) + && Objects.equals(autogeneratedUuidWriteAlways, that.autogeneratedUuidWriteAlways) + && Objects.equals(data, that.data) && Objects.equals(createdAt, that.createdAt) + && Objects.equals(updatedAt, that.updatedAt) + && Objects.equals(defaultCounter, that.defaultCounter) + && Objects.equals(customCounter, that.customCounter) + && Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(id, autogeneratedUuidWriteIfNotExists, autogeneratedUuidWriteAlways, data, createdAt, + updatedAt, defaultCounter, customCounter, version); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/VersionedRecordExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/VersionedRecordExtensionTest.java new file mode 100644 index 000000000000..50823e707ca6 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/VersionedRecordExtensionTest.java @@ -0,0 +1,243 @@ +/* + * 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.extensions; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +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.extensions.annotations.DynamoDbVersionAttribute; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +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.ConditionalCheckFailedException; + +public class VersionedRecordExtensionTest extends LocalDynamoDbSyncTestBase { + + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(VersionedRecord.class); + + private final DynamoDbEnhancedClient enhancedClient = + DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .build(); + + private final DynamoDbTable mappedTable = + enhancedClient.table(getConcreteTableName("versioned-table"), TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(r -> r.tableName(getConcreteTableName("versioned-table"))); + } + + @Test + public void putItem_setsInitialVersion() { + VersionedRecord record = new VersionedRecord(); + record.setId("id"); + record.setData("data"); + + mappedTable.putItem(record); + + VersionedRecord retrieved = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + assertThat(retrieved).isNotNull(); + assertThat(retrieved.getVersion()).isEqualTo(1L); + assertThat(retrieved.getData()).isEqualTo("data"); + } + + @Test + public void updateItem_incrementsVersion() { + VersionedRecord record = new VersionedRecord(); + record.setId("id"); + record.setData("data"); + mappedTable.putItem(record); + + VersionedRecord retrieved = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + retrieved.setData("data_update"); + mappedTable.updateItem(retrieved); + + VersionedRecord afterUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + assertThat(afterUpdate.getVersion()).isEqualTo(2L); + assertThat(afterUpdate.getData()).isEqualTo("data_update"); + } + + @Test + public void updateItem_withIncorrectVersion_throwsConditionalCheckFailedException() { + VersionedRecord record = new VersionedRecord(); + record.setId("id"); + record.setData("data"); + mappedTable.putItem(record); + + VersionedRecord retrieved1 = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + VersionedRecord retrieved2 = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + + retrieved1.setData("data_update1"); + mappedTable.updateItem(retrieved1); + + retrieved2.setData("data_update2"); + assertThatThrownBy(() -> mappedTable.updateItem(retrieved2)) + .isInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void putItem_withNullVersion_onExistingItem_throwsConditionalCheckFailedException() { + VersionedRecord record = new VersionedRecord(); + record.setId("id"); + record.setData("data"); + mappedTable.putItem(record); + VersionedRecord retrieved = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + assertThat(retrieved.getVersion()).isEqualTo(1L); + + retrieved.setData("data_update"); + mappedTable.updateItem(retrieved); + VersionedRecord afterUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + assertThat(afterUpdate.getVersion()).isEqualTo(2L); + + // Attempting to put an item with an existing version in the DB + VersionedRecord newRecord = new VersionedRecord(); + newRecord.setId("id"); + newRecord.setData("new-data"); + assertThatThrownBy(() -> mappedTable.putItem(newRecord)) + .isInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void multipleUpdates_incrementsVersionCorrectly() { + VersionedRecord record = new VersionedRecord(); + record.setId("id"); + mappedTable.putItem(record); + + for (int i = 1; i <= 5; i++) { + VersionedRecord retrieved = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + assertThat(retrieved.getVersion()).isEqualTo(i); + retrieved.setData("data-update-" + i); + mappedTable.updateItem(retrieved); + } + + VersionedRecord finalRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + assertThat(finalRecord.getVersion()).isEqualTo(6L); + assertThat(finalRecord.getData()).isEqualTo("data-update-5"); + } + + @Test + public void putItem_withExplicitVersion_throwsConditionalCheckFailedException() { + VersionedRecord record = new VersionedRecord(); + record.setId("id"); + record.setData("data"); + record.setVersion(100L); + + assertThatThrownBy(() -> mappedTable.putItem(record)) + .isInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void deleteItem_withCorrectVersion_succeeds() { + VersionedRecord record = new VersionedRecord(); + record.setId("id"); + mappedTable.putItem(record); + + VersionedRecord retrieved = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + mappedTable.deleteItem(retrieved); + + VersionedRecord afterDelete = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + assertThat(afterDelete).isNull(); + } + + @Test + public void deleteItem_withIncorrectVersion_succeeds() { + VersionedRecord record = new VersionedRecord(); + record.setId("id"); + mappedTable.putItem(record); + + VersionedRecord retrieved1 = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + VersionedRecord retrieved2 = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + + retrieved1.setData("data_update"); + mappedTable.updateItem(retrieved1); + + // This operation succeeds even if the two versions are incompatible because Optimistic Locking is not yet implemented + // for delete operations. + mappedTable.deleteItem(retrieved2); + VersionedRecord afterDelete = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + assertThat(afterDelete).isNull(); + } + + @DynamoDbBean + public static class VersionedRecord { + private String id; + private String data; + private Long version; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + @DynamoDbVersionAttribute + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VersionedRecord that = (VersionedRecord) o; + return Objects.equals(id, that.id) && + Objects.equals(data, that.data) && + Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(id, data, version); + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AbstractBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AbstractBean.java new file mode 100644 index 000000000000..5a345d8b3560 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AbstractBean.java @@ -0,0 +1,67 @@ +/* + * 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.models; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; + +@DynamoDbBean +public class AbstractBean { + + private String id; + + private String sort; + + private String stringAttribute; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + public String getStringAttribute() { + return stringAttribute; + } + + public void setStringAttribute(String stringAttribute) { + this.stringAttribute = stringAttribute; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + AbstractBean that = (AbstractBean) o; + return Objects.equals(id, that.id) && Objects.equals(sort, that.sort) && Objects.equals(stringAttribute, that.stringAttribute); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, stringAttribute); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AbstractImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AbstractImmutable.java new file mode 100644 index 000000000000..b1ceb9a8caba --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AbstractImmutable.java @@ -0,0 +1,90 @@ +/* + * 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.models; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; + +@DynamoDbImmutable(builder = AbstractImmutable.Builder.class) +public class AbstractImmutable { + + private final String id; + + private final String sort; + + private final String stringAttribute; + + private AbstractImmutable(Builder builder) { + id = builder.id; + sort = builder.sort; + stringAttribute = builder.stringAttribute; + } + + public String getId() { + return id; + } + + public String getSort() { + return sort; + } + + public String getStringAttribute() { + return stringAttribute; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + AbstractImmutable that = (AbstractImmutable) o; + return Objects.equals(id, that.id) && Objects.equals(sort, that.sort) && Objects.equals(stringAttribute, that.stringAttribute); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, stringAttribute); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String sort; + private String stringAttribute; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder sort(String sort) { + this.sort = sort; + return this; + } + + public Builder stringAttribute(String stringAttribute) { + this.stringAttribute = stringAttribute; + return this; + } + + public AbstractImmutable build() { + return new AbstractImmutable(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/SimpleBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/SimpleBean.java new file mode 100644 index 000000000000..60db805f34ed --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/SimpleBean.java @@ -0,0 +1,71 @@ +/* + * 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.models; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; + +@DynamoDbBean +public class SimpleBean { + + private String id; + + private String sort; + + private String stringAttribute; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + public String getStringAttribute() { + return stringAttribute; + } + + public void setStringAttribute(String stringAttribute) { + this.stringAttribute = stringAttribute; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleBean that = (SimpleBean) o; + return Objects.equals(id, that.id) && Objects.equals(sort, that.sort) && Objects.equals(stringAttribute, that.stringAttribute); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, stringAttribute); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/SimpleImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/SimpleImmutable.java new file mode 100644 index 000000000000..696a92d938ba --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/SimpleImmutable.java @@ -0,0 +1,94 @@ +/* + * 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.models; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; + +@DynamoDbImmutable(builder = SimpleImmutable.Builder.class) +public class SimpleImmutable { + + private final String id; + + private final String sort; + + private final String stringAttribute; + + private SimpleImmutable(Builder builder) { + id = builder.id; + sort = builder.sort; + stringAttribute = builder.stringAttribute; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbSortKey + public String getSort() { + return sort; + } + + public String getStringAttribute() { + return stringAttribute; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleImmutable that = (SimpleImmutable) o; + return Objects.equals(id, that.id) && Objects.equals(sort, that.sort) && Objects.equals(stringAttribute, that.stringAttribute); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, stringAttribute); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String sort; + private String stringAttribute; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder sort(String sort) { + this.sort = sort; + return this; + } + + public Builder stringAttribute(String stringAttribute) { + this.stringAttribute = stringAttribute; + return this; + } + + public SimpleImmutable build() { + return new SimpleImmutable(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java index 4577e0857eeb..d55b0d16a2c8 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java @@ -29,13 +29,18 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.ReadModification; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity; @RunWith(MockitoJUnitRunner.class) public class EnhancedClientUtilsTest { @@ -86,7 +91,6 @@ public void hasMap_forNotNullAttributeValueWithoutMap_returnsFalse() { @Test public void hasMap_forAttributeValueNull_returnsFalse() { - boolean result = EnhancedClientUtils.hasMap(null); assertThat(result).isFalse(); @@ -373,32 +377,111 @@ public void createKeyFromItem_withPartitionAndSortKey_createsCorrectKey() { } @Test - public void readAndTransformSingleItem_withNullItemMap_returnsNull() { - Object result = EnhancedClientUtils.readAndTransformSingleItem(null, mockSchema, null, null); + public void getItemsFromSupplier_withNullList_returnsNull() { + List result = EnhancedClientUtils.getItemsFromSupplier(null); assertThat(result).isNull(); } @Test - public void readAndTransformSingleItem_withEmptyItemMap_returnsNull() { - Map emptyMap = Collections.emptyMap(); - - Object result = EnhancedClientUtils.readAndTransformSingleItem(emptyMap, mockSchema, null, null); + public void getItemsFromSupplier_withEmptyList_returnsNull() { + List result = EnhancedClientUtils.getItemsFromSupplier(Collections.emptyList()); assertThat(result).isNull(); } + @Test - public void getItemsFromSupplier_withNullList_returnsNull() { - List result = EnhancedClientUtils.getItemsFromSupplier(null); + public void readAndTransformSingleItem_withNullItemMap_returnsNull() { + assertThat( + EnhancedClientUtils.readAndTransformSingleItem( + null, + FakeItem.getTableSchema(), + null, + null)) + .isNull(); + } - assertThat(result).isNull(); + @Test + public void readAndTransformSingleItem_withEmptyItemMap_returnsNull() { + assertThat( + EnhancedClientUtils.readAndTransformSingleItem( + Collections.emptyMap(), + FakeItem.getTableSchema(), + null, + null)) + .isNull(); } @Test - public void getItemsFromSupplier_withEmptyList_returnsNull() { - List result = EnhancedClientUtils.getItemsFromSupplier(Collections.emptyList()); + public void readAndTransformSingleItem_withExtensionAndTransformedItem_returnsTransformedItem() { + Map itemMap = new HashMap<>(); + itemMap.put("id", PARTITION_VALUE); + DynamoDbEnhancedClientExtension extension = new DynamoDbEnhancedClientExtension() { + @Override + public ReadModification afterRead(DynamoDbExtensionContext.AfterRead context) { + return ReadModification.builder().transformedItem(itemMap).build(); + } + }; - assertThat(result).isNull(); + assertThat( + EnhancedClientUtils.readAndTransformSingleItem( + itemMap, + FakeItem.getTableSchema(), + null, + extension)) + .isNotNull(); } -} \ No newline at end of file + + @Test + public void readAndTransformSingleItem_withExtensionNoTransformedItem_returnsOriginalItem() { + Map itemMap = new HashMap<>(); + itemMap.put("id", PARTITION_VALUE); + DynamoDbEnhancedClientExtension extension = new DynamoDbEnhancedClientExtension() {}; + + assertThat( + EnhancedClientUtils.readAndTransformSingleItem( + itemMap, + FakeItem.getTableSchema(), + null, + extension)) + .isNotNull(); + } + + @Test + public void readAndTransformPaginatedItems_withAllFields_returnsCompletePage() { + class TestResponse { + List> items; + Map lastKey; + int count; + int scannedCount; + ConsumedCapacity consumedCapacity; + } + TestResponse response = new TestResponse(); + Map itemMap = new HashMap<>(); + itemMap.put("id", PARTITION_VALUE); + response.items = Collections.singletonList(itemMap); + response.lastKey = new HashMap<>(); + response.lastKey.put("id", PARTITION_VALUE); + response.count = 1; + response.scannedCount = 1; + response.consumedCapacity = null; + + Page page = EnhancedClientUtils.readAndTransformPaginatedItems( + response, + FakeItem.getTableSchema(), + null, + null, + r -> r.items, + r -> r.lastKey, + r -> r.count, + r -> r.scannedCount, + r -> r.consumedCapacity + ); + + assertThat(page.items()).hasSize(1); + assertThat(page.count()).isEqualTo(1); + assertThat(page.scannedCount()).isEqualTo(1); + assertThat(page.lastEvaluatedKey()).isEqualTo(response.lastKey); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTableTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTableTest.java index dd5745b8c048..8d24d4a59411 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTableTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTableTest.java @@ -14,11 +14,7 @@ */ package software.amazon.awssdk.enhanced.dynamodb.internal.client; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.sameInstance; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -65,10 +61,11 @@ public void index_constructsCorrectMappedIndex() { DefaultDynamoDbAsyncIndex dynamoDbMappedIndex = dynamoDbMappedTable.index("gsi_1"); - assertThat(dynamoDbMappedIndex.dynamoDbClient(), is(sameInstance(mockDynamoDbAsyncClient))); - assertThat(dynamoDbMappedIndex.mapperExtension(), is(sameInstance(mockDynamoDbEnhancedClientExtension))); - assertThat(dynamoDbMappedIndex.tableSchema(), is(sameInstance(FakeItemWithIndices.getTableSchema()))); - assertThat(dynamoDbMappedIndex.indexName(), is("gsi_1")); + assertThat(dynamoDbMappedIndex.dynamoDbClient()).isSameAs(mockDynamoDbAsyncClient); + assertThat(dynamoDbMappedIndex.mapperExtension()).isSameAs(mockDynamoDbEnhancedClientExtension); + assertThat(dynamoDbMappedIndex.tableSchema()).isSameAs(FakeItemWithIndices.getTableSchema()); + assertThat(dynamoDbMappedIndex.tableName()).isEqualTo(TABLE_NAME); + assertThat(dynamoDbMappedIndex.indexName()).isEqualTo("gsi_1"); } @Test(expected = IllegalArgumentException.class) @@ -93,8 +90,8 @@ public void keyFrom_primaryIndex_partitionAndSort() { Key key = dynamoDbMappedIndex.keyFrom(item); - assertThat(key.partitionKeyValue(), is(stringValue(item.getId()))); - assertThat(key.sortKeyValue(), is(Optional.of(stringValue(item.getSort())))); + assertThat(key.partitionKeyValue()).isEqualTo(stringValue(item.getId())); + assertThat(key.sortKeyValue()).isEqualTo(Optional.of(stringValue(item.getSort()))); } @Test @@ -108,8 +105,8 @@ public void keyFrom_primaryIndex_partitionOnly() { Key key = dynamoDbMappedIndex.keyFrom(item); - assertThat(key.partitionKeyValue(), is(stringValue(item.getId()))); - assertThat(key.sortKeyValue(), is(Optional.empty())); + assertThat(key.partitionKeyValue()).isEqualTo(stringValue(item.getId())); + assertThat(key.sortKeyValue()).isEqualTo(Optional.empty()); } @Test @@ -123,8 +120,8 @@ public void keyFrom_primaryIndex_partitionAndNullSort() { Key key = dynamoDbMappedIndex.keyFrom(item); - assertThat(key.partitionKeyValue(), is(stringValue(item.getId()))); - assertThat(key.sortKeyValue(), is(Optional.empty())); + assertThat(key.partitionKeyValue()).isEqualTo(stringValue(item.getId())); + assertThat(key.sortKeyValue()).isEqualTo(Optional.empty()); } @Test @@ -145,8 +142,8 @@ public void createTable_doesNotTreatPrimaryIndexAsAnyOfSecondaryIndexes() { CreateTableRequest request = requestCaptor.getValue(); - assertThat(request.localSecondaryIndexes().size(), is(0)); - assertThat(request.globalSecondaryIndexes().size(), is(0)); + assertThat(request.localSecondaryIndexes().size()).isEqualTo(0); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(0); } @Test @@ -167,14 +164,15 @@ public void createTable_groupsSecondaryIndexesExistingInTableSchema() { CreateTableRequest request = requestCaptor.getValue(); - assertThat(request.localSecondaryIndexes().size(), is(1)); + assertThat(request.localSecondaryIndexes().size()).isEqualTo(1); Iterator lsiIterator = request.localSecondaryIndexes().iterator(); - assertThat(lsiIterator.next().indexName(), is("lsi_1")); + assertThat(dynamoDbMappedIndex.tableName()).isEqualTo("test_table"); + assertThat(lsiIterator.next().indexName()).isEqualTo("lsi_1"); - assertThat(request.globalSecondaryIndexes().size(), is(2)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(2); List globalIndicesNames = request.globalSecondaryIndexes().stream() .map(GlobalSecondaryIndex::indexName) .collect(Collectors.toList()); - assertThat(globalIndicesNames, containsInAnyOrder("gsi_1", "gsi_2")); + assertThat(globalIndicesNames).containsExactlyInAnyOrder("gsi_1", "gsi_2"); } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/BetweenConditionalTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/BetweenConditionalTest.java new file mode 100644 index 000000000000..e5a4df1c0ce4 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/BetweenConditionalTest.java @@ -0,0 +1,176 @@ +/* + * 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.internal.conditional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; + +class BetweenConditionalTest { + + private static final StaticTableSchema TABLE_SCHEMA = + StaticTableSchema.builder(TestItem.class) + .newItemSupplier(TestItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(TestItem::getId) + .setter(TestItem::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("sort") + .getter(TestItem::getSort) + .setter(TestItem::setSort) + .tags(primarySortKey())) + .build(); + + @Test + void expression_whenBothKeysHaveValidSortValues_generatesExpression() { + Key key1 = Key.builder() + .partitionValue("test") + .sortValue("sortA") + .build(); + Key key2 = Key.builder() + .partitionValue("test") + .sortValue("sortZ") + .build(); + + QueryConditional conditional = new BetweenConditional(key1, key2); + Expression expression = conditional.expression(TABLE_SCHEMA, TableMetadata.primaryIndexName()); + + assertThat(expression).isNotNull(); + assertThat(expression.expression()).contains("BETWEEN"); + } + + @Test + void expression_whenSecondKeySortValuesDoNotContainNull_generatesExpression() { + Key key1 = Key.builder().partitionValue("test").sortValue("sortA").build(); + Key key2 = Key.builder().partitionValue("test").sortValue("sortZ").build(); + + QueryConditional conditional = new BetweenConditional(key1, key2); + Expression expression = conditional.expression(TABLE_SCHEMA, TableMetadata.primaryIndexName()); + + assertThat(expression.expression()).contains("BETWEEN"); + } + + @Test + void expression_whenSecondKeySortValuesContainNull_throwsException() { + Key key1 = Key.builder().partitionValue("test").sortValue("sortA").build(); + Key key2 = Key.builder() + .partitionValue("test") + .sortValues(java.util.Collections.singletonList( + software.amazon.awssdk.services.dynamodb.model.AttributeValue.builder().nul(true).build())) + .build(); + + BetweenConditional conditional = new BetweenConditional(key1, key2); + + assertThatThrownBy(() -> conditional.expression(TABLE_SCHEMA, TableMetadata.primaryIndexName())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void expression_whenSecondKeyHasNullSortValue_throwsException() { + Key key1 = Key.builder().partitionValue("test").sortValue("sortA").build(); + Key key2 = Key.builder().partitionValue("test").build(); + + BetweenConditional conditional = new BetweenConditional(key1, key2); + + assertThatThrownBy(() -> conditional.expression(TABLE_SCHEMA, TableMetadata.primaryIndexName())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void expression_whenFirstKeyHasNullSortValue_throwsException() { + Key key1 = Key.builder().partitionValue("test").build(); + Key key2 = Key.builder().partitionValue("test").sortValue("sortZ").build(); + + BetweenConditional conditional = new BetweenConditional(key1, key2); + + assertThatThrownBy(() -> conditional.expression(TABLE_SCHEMA, TableMetadata.primaryIndexName())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void expression_whenNumericSortValues_generatesExpression() { + Key key1 = Key.builder().partitionValue("test").sortValue(100).build(); + Key key2 = Key.builder().partitionValue("test").sortValue(200).build(); + + QueryConditional conditional = new BetweenConditional(key1, key2); + Expression expression = conditional.expression(TABLE_SCHEMA, TableMetadata.primaryIndexName()); + + assertThat(expression.expression()).contains("BETWEEN"); + } + + @Test + void equals_whenKeysAreIdentical_returnsTrue() { + Key key1 = Key.builder().partitionValue("test").sortValue("sortA").build(); + Key key2 = Key.builder().partitionValue("test").sortValue("sortZ").build(); + + BetweenConditional conditional1 = new BetweenConditional(key1, key2); + BetweenConditional conditional2 = new BetweenConditional(key1, key2); + + assertThat(conditional1).isEqualTo(conditional2); + } + + @Test + void equals_whenKeysAreDifferent_returnsFalse() { + Key key1 = Key.builder().partitionValue("test").sortValue("sortA").build(); + Key key2 = Key.builder().partitionValue("test").sortValue("sortZ").build(); + Key key3 = Key.builder().partitionValue("test").sortValue("sortX").build(); + + BetweenConditional conditional1 = new BetweenConditional(key1, key2); + BetweenConditional conditional2 = new BetweenConditional(key1, key3); + + assertThat(conditional1).isNotEqualTo(conditional2); + } + + @Test + void hashCode_whenKeysAreIdentical_returnsSameHashCode() { + Key key1 = Key.builder().partitionValue("test").sortValue("sortA").build(); + Key key2 = Key.builder().partitionValue("test").sortValue("sortZ").build(); + + BetweenConditional conditional1 = new BetweenConditional(key1, key2); + BetweenConditional conditional2 = new BetweenConditional(key1, key2); + + assertThat(conditional1.hashCode()).isEqualTo(conditional2.hashCode()); + } + + private static class TestItem { + private String id; + private String sort; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/OptionalAttributeValueConverterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/OptionalAttributeValueConverterTest.java index b7c9ee6d3abf..93fb641ddc98 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/OptionalAttributeValueConverterTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/OptionalAttributeValueConverterTest.java @@ -18,7 +18,9 @@ import org.junit.jupiter.api.Test; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; -public class OptionalAttributeValueConverterTest { +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class OptionalAttributeValueConverterTest { private static final OptionalAttributeConverter CONVERTER = OptionalAttributeConverter.create(StringAttributeConverter.create()); @Test @@ -28,6 +30,6 @@ public void testTransformTo_nulPropertyIsNull_doesNotThrowNPE() { .s("foo") .build(); - CONVERTER.transformTo(av); + assertDoesNotThrow(() -> CONVERTER.transformTo(av)); } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanAttributeGetterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanAttributeGetterTest.java new file mode 100644 index 000000000000..a48bc9c85367 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanAttributeGetterTest.java @@ -0,0 +1,58 @@ +/* + * 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.internal.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import org.junit.jupiter.api.Test; + +public class BeanAttributeGetterTest { + + @Test + public void create_validGetter_succeeds() throws Exception { + Method getter = ValidBean.class.getDeclaredMethod("getValue"); + + BeanAttributeGetter attributeGetter = BeanAttributeGetter.create( + ValidBean.class, getter, MethodHandles.lookup()); + + ValidBean bean = new ValidBean(); + assertThat(attributeGetter.apply(bean)).isEqualTo("test"); + } + + @Test + public void create_getterWithParameters_throwsException() throws Exception { + Method getter = InvalidBean.class.getDeclaredMethod("getValue", String.class); + + assertThatThrownBy(() -> BeanAttributeGetter.create(InvalidBean.class, getter, MethodHandles.lookup())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("has parameters, despite being named like a getter"); + } + + public static class ValidBean { + public String getValue() { + return "test"; + } + } + + public static class InvalidBean { + public String getValue(String param) { + return param; + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanAttributeSetterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanAttributeSetterTest.java new file mode 100644 index 000000000000..745daca94219 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanAttributeSetterTest.java @@ -0,0 +1,64 @@ +/* + * 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.internal.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import org.junit.jupiter.api.Test; + +public class BeanAttributeSetterTest { + + @Test + public void create_validSetter_succeeds() throws Exception { + Method setter = ValidBean.class.getDeclaredMethod("setValue", String.class); + + BeanAttributeSetter attributeSetter = BeanAttributeSetter.create( + ValidBean.class, setter, MethodHandles.lookup()); + + ValidBean bean = new ValidBean(); + attributeSetter.accept(bean, "newValue"); + assertThat(bean.getValue()).isEqualTo("newValue"); + } + + @Test + public void create_setterWithNoParameters_throwsException() throws Exception { + Method setter = InvalidBean.class.getDeclaredMethod("setValue"); + + assertThatThrownBy(() -> BeanAttributeSetter.create(InvalidBean.class, setter, MethodHandles.lookup())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("doesn't have just 1 parameter, despite being named like a setter"); + } + + public static class ValidBean { + private String value; + + public void setValue(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + public static class InvalidBean { + public void setValue() { + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectConstructorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectConstructorTest.java new file mode 100644 index 000000000000..bb7ff6d63245 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectConstructorTest.java @@ -0,0 +1,55 @@ +/* + * 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.internal.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import org.junit.jupiter.api.Test; + +public class ObjectConstructorTest { + + @Test + public void create_validNoArgsConstructor_succeeds() throws Exception { + Constructor constructor = ValidBean.class.getDeclaredConstructor(); + + ObjectConstructor objectConstructor = ObjectConstructor.create( + ValidBean.class, constructor, MethodHandles.lookup()); + + assertThat(objectConstructor.get()).isInstanceOf(ValidBean.class); + } + + @Test + public void create_constructorWithParameters_throwsException() throws Exception { + Constructor constructor = InvalidBean.class.getDeclaredConstructor(String.class); + + assertThatThrownBy(() -> ObjectConstructor.create(InvalidBean.class, constructor, MethodHandles.lookup())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("has no default constructor"); + } + + public static class ValidBean { + public ValidBean() { + } + } + + public static class InvalidBean { + public InvalidBean(String param) { + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticIndexMetadataTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticIndexMetadataTest.java new file mode 100644 index 000000000000..9259819f0620 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticIndexMetadataTest.java @@ -0,0 +1,380 @@ +/* + * 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.internal.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata; +import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticIndexMetadata.Builder; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; + +class StaticIndexMetadataTest { + + @Test + void builder_shouldReturnNewBuilderInstance() { + Builder builder = StaticIndexMetadata.builder(); + assertThat(builder).isNotNull(); + } + + @Test + void builderFrom_withNullIndex_shouldReturnEmptyBuilder() { + Builder builder = StaticIndexMetadata.builderFrom(null); + + assertThat(builder).isNotNull(); + assertThat(builder.build().name()).isNull(); + assertThat(builder.build().partitionKeys()).isEmpty(); + assertThat(builder.build().sortKeys()).isEmpty(); + } + + @Test + void builderFrom_withValidIndex_shouldCopyAllProperties() { + IndexMetadata sourceIndex = mock(IndexMetadata.class); + KeyAttributeMetadata partitionKey = createMockKeyAttribute(Order.FIRST); + KeyAttributeMetadata sortKey = createMockKeyAttribute(Order.FIRST); + + when(sourceIndex.name()).thenReturn("test-index"); + when(sourceIndex.partitionKeys()).thenReturn(Collections.singletonList(partitionKey)); + when(sourceIndex.sortKeys()).thenReturn(Collections.singletonList(sortKey)); + + StaticIndexMetadata result = StaticIndexMetadata.builderFrom(sourceIndex).build(); + + assertThat(result.name()).isEqualTo("test-index"); + assertThat(result.partitionKeys()).containsExactly(partitionKey); + assertThat(result.sortKeys()).containsExactly(sortKey); + } + + @Test + void build_shouldSortPartitionKeysByOrder() { + KeyAttributeMetadata key1 = createMockKeyAttribute(Order.THIRD); + KeyAttributeMetadata key2 = createMockKeyAttribute(Order.FIRST); + KeyAttributeMetadata key3 = createMockKeyAttribute(Order.SECOND); + + StaticIndexMetadata result = StaticIndexMetadata.builder() + .partitionKeys(Arrays.asList(key1, key2, key3)) + .build(); + + assertThat(result.partitionKeys()).containsExactly(key2, key3, key1); + } + + @Test + void build_shouldSortSortKeysByOrder() { + KeyAttributeMetadata key1 = createMockKeyAttribute(Order.THIRD); + KeyAttributeMetadata key2 = createMockKeyAttribute(Order.FIRST); + KeyAttributeMetadata key3 = createMockKeyAttribute(Order.SECOND); + + StaticIndexMetadata result = StaticIndexMetadata.builder() + .sortKeys(Arrays.asList(key1, key2, key3)) + .build(); + + assertThat(result.sortKeys()).containsExactly(key2, key3, key1); + } + + @Test + void name_shouldReturnConfiguredName() { + StaticIndexMetadata metadata = StaticIndexMetadata.builder() + .name("test-name") + .build(); + + assertThat(metadata.name()).isEqualTo("test-name"); + } + + @Test + void name_shouldReturnNullWhenNotSet() { + StaticIndexMetadata metadata = StaticIndexMetadata.builder().build(); + assertThat(metadata.name()).isNull(); + } + + @Test + void partitionKeys_shouldReturnUnmodifiableList() { + KeyAttributeMetadata key = createMockKeyAttribute(Order.FIRST); + StaticIndexMetadata metadata = StaticIndexMetadata.builder() + .addPartitionKey(key) + .build(); + + List partitionKeys = metadata.partitionKeys(); + assertThat(partitionKeys).containsExactly(key); + } + + @Test + void sortKeys_shouldReturnUnmodifiableList() { + KeyAttributeMetadata key = createMockKeyAttribute(Order.FIRST); + StaticIndexMetadata metadata = StaticIndexMetadata.builder() + .addSortKey(key) + .build(); + + List sortKeys = metadata.sortKeys(); + assertThat(sortKeys).containsExactly(key); + } + + @Test + void builderName_shouldSetName() { + Builder builder = StaticIndexMetadata.builder().name("test"); + assertThat(builder.build().name()).isEqualTo("test"); + } + + @Test + void builderPartitionKeys_shouldReplaceExistingKeys() { + KeyAttributeMetadata key1 = createMockKeyAttribute(Order.FIRST); + KeyAttributeMetadata key2 = createMockKeyAttribute(Order.SECOND); + + Builder builder = StaticIndexMetadata.builder() + .addPartitionKey(key1) + .partitionKeys(Collections.singletonList(key2)); + + assertThat(builder.build().partitionKeys()).containsExactly(key2); + } + + @Test + void builderSortKeys_shouldReplaceExistingKeys() { + KeyAttributeMetadata key1 = createMockKeyAttribute(Order.FIRST); + KeyAttributeMetadata key2 = createMockKeyAttribute(Order.SECOND); + + Builder builder = StaticIndexMetadata.builder() + .addSortKey(key1) + .sortKeys(Collections.singletonList(key2)); + + assertThat(builder.build().sortKeys()).containsExactly(key2); + } + + @Test + void builderAddPartitionKey_shouldAppendKey() { + KeyAttributeMetadata key1 = createMockKeyAttribute(Order.FIRST); + KeyAttributeMetadata key2 = createMockKeyAttribute(Order.SECOND); + + Builder builder = StaticIndexMetadata.builder() + .addPartitionKey(key1) + .addPartitionKey(key2); + + assertThat(builder.build().partitionKeys()).containsExactly(key1, key2); + } + + @Test + void builderAddSortKey_shouldAppendKey() { + KeyAttributeMetadata key1 = createMockKeyAttribute(Order.FIRST); + KeyAttributeMetadata key2 = createMockKeyAttribute(Order.SECOND); + + Builder builder = StaticIndexMetadata.builder() + .addSortKey(key1) + .addSortKey(key2); + + assertThat(builder.build().sortKeys()).containsExactly(key1, key2); + } + + @Test + void builderGetPartitionKeys_shouldReturnCopyOfKeys() { + KeyAttributeMetadata key = createMockKeyAttribute(Order.FIRST); + Builder builder = StaticIndexMetadata.builder().addPartitionKey(key); + + List keys = builder.getPartitionKeys(); + + assertThat(keys).containsExactly(key); + keys.clear(); + assertThat(builder.getPartitionKeys()).containsExactly(key); + } + + @Test + void builderGetSortKeys_shouldReturnCopyOfKeys() { + KeyAttributeMetadata key = createMockKeyAttribute(Order.FIRST); + Builder builder = StaticIndexMetadata.builder().addSortKey(key); + + List keys = builder.getSortKeys(); + + assertThat(keys).containsExactly(key); + keys.clear(); + assertThat(builder.getSortKeys()).containsExactly(key); + } + + @Test + void builderPartitionKey_shouldReplaceWithSingleKey() { + KeyAttributeMetadata key1 = createMockKeyAttribute(Order.FIRST); + KeyAttributeMetadata key2 = createMockKeyAttribute(Order.SECOND); + + Builder builder = StaticIndexMetadata.builder() + .addPartitionKey(key1) + .partitionKey(key2); + + assertThat(builder.build().partitionKeys()).containsExactly(key2); + } + + @Test + void builderPartitionKey_withNull_shouldClearKeys() { + KeyAttributeMetadata key = createMockKeyAttribute(Order.FIRST); + + Builder builder = StaticIndexMetadata.builder() + .addPartitionKey(key) + .partitionKey(null); + + assertThat(builder.build().partitionKeys()).isEmpty(); + } + + @Test + void builderSortKey_shouldReplaceWithSingleKey() { + KeyAttributeMetadata key1 = createMockKeyAttribute(Order.FIRST); + KeyAttributeMetadata key2 = createMockKeyAttribute(Order.SECOND); + + Builder builder = StaticIndexMetadata.builder() + .addSortKey(key1) + .sortKey(key2); + + assertThat(builder.build().sortKeys()).containsExactly(key2); + } + + @Test + void builderSortKey_withNull_shouldClearKeys() { + KeyAttributeMetadata key = createMockKeyAttribute(Order.FIRST); + + Builder builder = StaticIndexMetadata.builder() + .addSortKey(key) + .sortKey(null); + + assertThat(builder.build().sortKeys()).isEmpty(); + } + + @Test + void equals_withSameInstance_shouldReturnTrue() { + StaticIndexMetadata metadata = StaticIndexMetadata.builder().build(); + assertThat(metadata.equals(metadata)).isTrue(); + } + + @Test + void equals_withNull_shouldReturnFalse() { + StaticIndexMetadata metadata = StaticIndexMetadata.builder().build(); + assertThat(metadata.equals(null)).isFalse(); + } + + @Test + void equals_withDifferentClass_shouldReturnFalse() { + StaticIndexMetadata metadata = StaticIndexMetadata.builder().build(); + assertThat(metadata.equals("string")).isFalse(); + } + + @Test + void equals_withSameProperties_shouldReturnTrue() { + KeyAttributeMetadata key = createMockKeyAttribute(Order.FIRST); + StaticIndexMetadata metadata1 = StaticIndexMetadata.builder() + .name("test") + .addPartitionKey(key) + .addSortKey(key) + .build(); + StaticIndexMetadata metadata2 = StaticIndexMetadata.builder() + .name("test") + .addPartitionKey(key) + .addSortKey(key) + .build(); + + assertThat(metadata1.equals(metadata2)).isTrue(); + } + + @Test + void equals_withDifferentName_shouldReturnFalse() { + StaticIndexMetadata metadata1 = StaticIndexMetadata.builder().name("test1").build(); + StaticIndexMetadata metadata2 = StaticIndexMetadata.builder().name("test2").build(); + assertThat(metadata1.equals(metadata2)).isFalse(); + } + + @Test + void equals_withNullNameVsNonNull_shouldReturnFalse() { + StaticIndexMetadata metadata1 = StaticIndexMetadata.builder().build(); + StaticIndexMetadata metadata2 = StaticIndexMetadata.builder().name("test").build(); + assertThat(metadata1.equals(metadata2)).isFalse(); + } + + @Test + void equals_withNonNullNameVsNull_shouldReturnFalse() { + StaticIndexMetadata metadata1 = StaticIndexMetadata.builder().name("test").build(); + StaticIndexMetadata metadata2 = StaticIndexMetadata.builder().build(); + assertThat(metadata1.equals(metadata2)).isFalse(); + } + + @Test + void equals_withBothNullNames_shouldReturnTrue() { + KeyAttributeMetadata key = createMockKeyAttribute(Order.FIRST); + StaticIndexMetadata metadata1 = StaticIndexMetadata.builder().addPartitionKey(key).build(); + StaticIndexMetadata metadata2 = StaticIndexMetadata.builder().addPartitionKey(key).build(); + assertThat(metadata1.equals(metadata2)).isTrue(); + } + + @Test + void equals_withDifferentPartitionKeys_shouldReturnFalse() { + KeyAttributeMetadata key1 = createMockKeyAttribute(Order.FIRST); + KeyAttributeMetadata key2 = createMockKeyAttribute(Order.SECOND); + + StaticIndexMetadata metadata1 = StaticIndexMetadata.builder().addPartitionKey(key1).build(); + StaticIndexMetadata metadata2 = StaticIndexMetadata.builder().addPartitionKey(key2).build(); + assertThat(metadata1.equals(metadata2)).isFalse(); + } + + @Test + void equals_withDifferentSortKeys_shouldReturnFalse() { + KeyAttributeMetadata key1 = createMockKeyAttribute(Order.FIRST); + KeyAttributeMetadata key2 = createMockKeyAttribute(Order.SECOND); + + StaticIndexMetadata metadata1 = StaticIndexMetadata.builder().addSortKey(key1).build(); + StaticIndexMetadata metadata2 = StaticIndexMetadata.builder().addSortKey(key2).build(); + assertThat(metadata1.equals(metadata2)).isFalse(); + } + + @Test + void hashCode_withSameProperties_shouldReturnSameValue() { + KeyAttributeMetadata key = createMockKeyAttribute(Order.FIRST); + StaticIndexMetadata metadata1 = StaticIndexMetadata.builder() + .name("test") + .addPartitionKey(key) + .addSortKey(key) + .build(); + StaticIndexMetadata metadata2 = StaticIndexMetadata.builder() + .name("test") + .addPartitionKey(key) + .addSortKey(key) + .build(); + + assertThat(metadata1.hashCode()).isEqualTo(metadata2.hashCode()); + } + + @Test + void hashCode_withNullName_shouldNotThrow() { + StaticIndexMetadata metadata = StaticIndexMetadata.builder().build(); + assertThat(metadata.hashCode()).isNotNull(); + } + + @Test + void hashCode_withMultipleKeys_shouldIncludeAllKeys() { + KeyAttributeMetadata key1 = createMockKeyAttribute(Order.FIRST); + KeyAttributeMetadata key2 = createMockKeyAttribute(Order.SECOND); + + StaticIndexMetadata metadata = StaticIndexMetadata.builder() + .addPartitionKey(key1) + .addPartitionKey(key2) + .addSortKey(key1) + .addSortKey(key2) + .build(); + + assertThat(metadata.hashCode()).isNotNull(); + } + + private KeyAttributeMetadata createMockKeyAttribute(Order order) { + KeyAttributeMetadata mock = mock(KeyAttributeMetadata.class); + when(mock.order()).thenReturn(order); + return mock; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/BatchGetItemOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/BatchGetItemOperationTest.java index a95b3aebda8a..87f4aaeb92c2 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/BatchGetItemOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/BatchGetItemOperationTest.java @@ -15,17 +15,12 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; -import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.stream.Collectors.toList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.sameInstance; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doReturn; @@ -37,16 +32,24 @@ import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort.createUniqueFakeItemWithSort; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.IntStream; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.MockitoRule; +import software.amazon.awssdk.core.async.SdkPublisher; import software.amazon.awssdk.core.pagination.sync.SdkIterable; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; @@ -59,18 +62,24 @@ import software.amazon.awssdk.enhanced.dynamodb.model.BatchGetResultPage; import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.ReadBatch; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest; import software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse; import software.amazon.awssdk.services.dynamodb.model.KeysAndAttributes; import software.amazon.awssdk.services.dynamodb.paginators.BatchGetItemIterable; +import software.amazon.awssdk.services.dynamodb.paginators.BatchGetItemPublisher; -@RunWith(MockitoJUnitRunner.class) +@RunWith(Enclosed.class) public class BatchGetItemOperationTest { + private static final String TABLE_NAME = "table-name"; private static final String TABLE_NAME_2 = "table-name-2"; + @RunWith(MockitoJUnitRunner.class) + public static class Standard { + private static final List FAKE_ITEMS = IntStream.range(0, 6).mapToObj($ -> createUniqueFakeItem()).collect(toList()); private static final List> FAKE_ITEM_MAPS = FAKE_ITEMS.stream().map(item -> @@ -107,6 +116,12 @@ public void setupMappedTables() { fakeItemWithSortMappedTable = enhancedClient.table(TABLE_NAME_2, FakeItemWithSort.getTableSchema()); } + @Test + public void returnsCorrectOperationName() { + BatchGetItemOperation operation = BatchGetItemOperation.create(emptyRequest()); + assertThat(operation.operationName().label()).isEqualTo("BatchGetItem"); + } + @Test public void getServiceCall_usingShortcutForm_makesTheRightCallAndReturnsResponse() { BatchGetItemEnhancedRequest batchGetItemEnhancedRequest = @@ -133,7 +148,7 @@ public void getServiceCall_usingShortcutForm_makesTheRightCallAndReturnsResponse SdkIterable response = operation.serviceCall(mockDynamoDbClient).apply(batchGetItemRequest); - assertThat(response, sameInstance(expectedResponse)); + assertThat(response).isSameAs(expectedResponse); verify(mockDynamoDbClient).batchGetItemPaginator(batchGetItemRequest); } @@ -163,7 +178,7 @@ public void getServiceCall_usingKeyItemForm_makesTheRightCallAndReturnsResponse( SdkIterable response = operation.serviceCall(mockDynamoDbClient).apply(batchGetItemRequest); - assertThat(response, sameInstance(expectedResponse)); + assertThat(response).isSameAs(expectedResponse); verify(mockDynamoDbClient).batchGetItemPaginator(batchGetItemRequest); } @@ -192,10 +207,10 @@ public void generateRequest_multipleBatches_multipleTableSchemas() { KeysAndAttributes keysAndAttributes1 = batchGetItemRequest.requestItems().get(TABLE_NAME); KeysAndAttributes keysAndAttributes2 = batchGetItemRequest.requestItems().get(TABLE_NAME_2); - assertThat(keysAndAttributes1.keys(), containsInAnyOrder(FAKE_ITEM_MAPS.subList(0, 3).toArray())); - assertThat(keysAndAttributes2.keys(), containsInAnyOrder(FAKESORT_ITEM_MAPS.subList(0, 3).toArray())); - assertThat(keysAndAttributes1.consistentRead(), is(nullValue())); - assertThat(keysAndAttributes2.consistentRead(), is(nullValue())); + assertThat(keysAndAttributes1.keys()).containsExactlyInAnyOrderElementsOf(FAKE_ITEM_MAPS.subList(0, 3)); + assertThat(keysAndAttributes2.keys()).containsExactlyInAnyOrderElementsOf(FAKESORT_ITEM_MAPS.subList(0, 3)); + assertThat(keysAndAttributes1.consistentRead()).isNull(); + assertThat(keysAndAttributes2.consistentRead()).isNull(); verifyNoMoreInteractions(mockExtension); } @@ -224,10 +239,10 @@ public void generateRequest_multipleBatches_multipleTableSchemas_nonConflictingC KeysAndAttributes keysAndAttributes1 = batchGetItemRequest.requestItems().get(TABLE_NAME); KeysAndAttributes keysAndAttributes2 = batchGetItemRequest.requestItems().get(TABLE_NAME_2); - assertThat(keysAndAttributes1.keys(), containsInAnyOrder(FAKE_ITEM_MAPS.subList(0, 3).toArray())); - assertThat(keysAndAttributes2.keys(), containsInAnyOrder(FAKESORT_ITEM_MAPS.subList(0, 3).toArray())); - assertThat(keysAndAttributes1.consistentRead(), is(true)); - assertThat(keysAndAttributes2.consistentRead(), is(false)); + assertThat(keysAndAttributes1.keys()).containsExactlyInAnyOrderElementsOf(FAKE_ITEM_MAPS.subList(0, 3)); + assertThat(keysAndAttributes2.keys()).containsExactlyInAnyOrderElementsOf(FAKESORT_ITEM_MAPS.subList(0, 3)); + assertThat(keysAndAttributes1.consistentRead()).isEqualTo(true); + assertThat(keysAndAttributes2.consistentRead()).isEqualTo(false); } @Test(expected = IllegalArgumentException.class) @@ -294,8 +309,8 @@ public void transformResponse_multipleTables_multipleItems_noExtension() { List fakeItemWithSortResultsPage = resultsPage.resultsForTable(fakeItemWithSortMappedTable); - assertThat(fakeItemResultsPage, containsInAnyOrder(FAKE_ITEMS.get(0), FAKE_ITEMS.get(1))); - assertThat(fakeItemWithSortResultsPage, containsInAnyOrder(FAKESORT_ITEMS.get(0))); + assertThat(fakeItemResultsPage).containsExactlyInAnyOrder(FAKE_ITEMS.get(0), FAKE_ITEMS.get(1)); + assertThat(fakeItemWithSortResultsPage).containsExactlyInAnyOrder(FAKESORT_ITEMS.get(0)); } @Test @@ -325,8 +340,8 @@ public void transformResponse_multipleTables_multipleItems_unprocessedKeys() { List fakeItemResults1 = resultsPage.unprocessedKeysForTable(fakeItemMappedTable); List fakeItemResults2 = resultsPage.unprocessedKeysForTable(fakeItemWithSortMappedTable); - assertThat(fakeItemResults1, containsInAnyOrder(FAKE_ITEM_KEYS.get(0), FAKE_ITEM_KEYS.get(1))); - assertThat(fakeItemResults2, containsInAnyOrder(FAKESORT_ITEM_KEYS.get(0))); + assertThat(fakeItemResults1).containsExactlyInAnyOrder(FAKE_ITEM_KEYS.get(0), FAKE_ITEM_KEYS.get(1)); + assertThat(fakeItemResults2).containsExactlyInAnyOrder(FAKESORT_ITEM_KEYS.get(0)); } @Test @@ -339,8 +354,8 @@ public void transformResponse_multipleTables_multipleItems_no_unprocessedKeys() List fakeItemResults1 = resultsPage.unprocessedKeysForTable(fakeItemMappedTable); List fakeItemResults2 = resultsPage.unprocessedKeysForTable(fakeItemWithSortMappedTable); - assertThat(fakeItemResults1, empty()); - assertThat(fakeItemResults2, empty()); + assertThat(fakeItemResults1).isEmpty(); + assertThat(fakeItemResults2).isEmpty(); } @Test @@ -364,7 +379,7 @@ public void transformResponse_multipleTables_multipleItems_unprocessedKeys_table BatchGetResultPage resultsPage = operation.transformResponse(fakeResponse, null); List fakeItemResults = resultsPage.unprocessedKeysForTable(fakeItemWithSortMappedTable); - assertThat(fakeItemResults, empty()); + assertThat(fakeItemResults).isEmpty(); } @Test @@ -381,14 +396,16 @@ public void transformResponse_multipleTables_multipleItems_extensionWithTransfor .when(mockExtension) .afterRead( argThat(extensionContext -> - extensionContext.operationContext().tableName().equals(TABLE_NAME) && - extensionContext.items().equals(FAKE_ITEM_MAPS.get(i)) + extensionContext.tableSchema().equals(FakeItem.getTableSchema()) + && extensionContext.operationContext().tableName().equals(TABLE_NAME) + && extensionContext.items().equals(FAKE_ITEM_MAPS.get(i)) )); doReturn(ReadModification.builder().transformedItem(FAKESORT_ITEM_MAPS.get(i + 3)).build()) .when(mockExtension) .afterRead(argThat(extensionContext -> - extensionContext.operationContext().tableName().equals(TABLE_NAME_2) && - extensionContext.items().equals(FAKESORT_ITEM_MAPS.get(i)) + extensionContext.tableSchema().equals(FakeItemWithSort.getTableSchema()) + && extensionContext.operationContext().tableName().equals(TABLE_NAME_2) + && extensionContext.items().equals(FAKESORT_ITEM_MAPS.get(i)) )); }); @@ -399,8 +416,8 @@ public void transformResponse_multipleTables_multipleItems_extensionWithTransfor resultsPage.resultsForTable(fakeItemWithSortMappedTable); - assertThat(fakeItemResultsPage, containsInAnyOrder(FAKE_ITEMS.get(3), FAKE_ITEMS.get(4))); - assertThat(fakeItemWithSortResultsPage, containsInAnyOrder(FAKESORT_ITEMS.get(3))); + assertThat(fakeItemResultsPage).containsExactlyInAnyOrder(FAKE_ITEMS.get(3), FAKE_ITEMS.get(4)); + assertThat(fakeItemWithSortResultsPage).containsExactlyInAnyOrder(FAKESORT_ITEMS.get(3)); } @Test @@ -410,7 +427,123 @@ public void transformResponse_queryingEmptyResults() { BatchGetResultPage resultsPage = operation.transformResponse(fakeResults, null); - assertThat(resultsPage.resultsForTable(fakeItemMappedTable), is(emptyList())); + assertThat(resultsPage.resultsForTable(fakeItemMappedTable)).isEmpty(); + } + + @Test + public void generateRequest_mergesKeysAndAttributes_bothConsistentReadNull() { + ReadBatch batch1 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(Key.builder().partitionValue("1").build()) + .build(); + ReadBatch batch2 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(Key.builder().partitionValue("2").build()) + .build(); + BatchGetItemEnhancedRequest req = BatchGetItemEnhancedRequest.builder() + .readBatches(batch1, batch2) + .build(); + BatchGetItemOperation op = BatchGetItemOperation.create(req); + + BatchGetItemRequest result = op.generateRequest(null); + + List> keys = result.requestItems().get(TABLE_NAME).keys(); + assertThat(keys).containsExactlyInAnyOrder(FakeItem.getTableSchema().itemToMap(FakeItem.builder().id("1").build(), FakeItem.getTableMetadata().primaryKeys()), + FakeItem.getTableSchema().itemToMap(FakeItem.builder().id("2").build(), FakeItem.getTableMetadata().primaryKeys())); + assertThat(result.requestItems().get(TABLE_NAME).consistentRead()).isNull(); + } + + @Test + public void generateRequest_mergesKeysAndAttributes_consistentReadTrue() { + ReadBatch batch1 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(GetItemEnhancedRequest.builder() + .key(Key.builder().partitionValue("1").build()) + .consistentRead(true) + .build()) + .build(); + ReadBatch batch2 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(GetItemEnhancedRequest.builder() + .key(Key.builder().partitionValue("2").build()) + .consistentRead(true) + .build()) + .build(); + BatchGetItemEnhancedRequest req = BatchGetItemEnhancedRequest.builder() + .readBatches(batch1, batch2) + .build(); + BatchGetItemOperation op = BatchGetItemOperation.create(req); + + BatchGetItemRequest result = op.generateRequest(null); + + List> keys = result.requestItems().get(TABLE_NAME).keys(); + assertThat(keys).containsExactlyInAnyOrder(FakeItem.getTableSchema().itemToMap(FakeItem.builder().id("1").build(), FakeItem.getTableMetadata().primaryKeys()), + FakeItem.getTableSchema().itemToMap(FakeItem.builder().id("2").build(), FakeItem.getTableMetadata().primaryKeys())); + assertThat(result.requestItems().get(TABLE_NAME).consistentRead()).isEqualTo(true); + } + + @Test + public void generateRequest_allowsAllNullConsistentReadAcrossBatches() { + ReadBatch batch1 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(Key.builder().partitionValue("1").build()) + .build(); + ReadBatch batch2 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(Key.builder().partitionValue("2").build()) + .build(); + BatchGetItemEnhancedRequest request = BatchGetItemEnhancedRequest.builder() + .readBatches(batch1, batch2) + .build(); + BatchGetItemOperation operation = BatchGetItemOperation.create(request); + operation.generateRequest(null); // Should not throw + } + + @Test + public void generateRequest_allowsAllTrueConsistentReadAcrossBatches() { + ReadBatch batch1 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(GetItemEnhancedRequest.builder().key(Key.builder().partitionValue("7").build()).consistentRead(true).build()) + .build(); + ReadBatch batch2 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(GetItemEnhancedRequest.builder().key(Key.builder().partitionValue("8").build()).consistentRead(true).build()) + .build(); + BatchGetItemEnhancedRequest request = BatchGetItemEnhancedRequest.builder() + .readBatches(batch1, batch2) + .build(); + BatchGetItemOperation operation = BatchGetItemOperation.create(request); + operation.generateRequest(null); // Should not throw + } + + @Test + public void serviceCall_returnsSyncPaginatorFunction() { + BatchGetItemOperation operation = BatchGetItemOperation.create(emptyRequest()); + DynamoDbClient mockSyncClient = mock(DynamoDbClient.class); + BatchGetItemRequest request = BatchGetItemRequest.builder().build(); + BatchGetItemIterable mockIterable = mock(BatchGetItemIterable.class); + when(mockSyncClient.batchGetItemPaginator(request)).thenReturn(mockIterable); + + Function> syncCall = operation.serviceCall(mockSyncClient); + SdkIterable result = syncCall.apply(request); + + assertThat(result).isEqualTo(mockIterable); + verify(mockSyncClient).batchGetItemPaginator(request); + } + + @Test + public void asyncServiceCall_returnsAsyncPaginatorFunction_publicApi() { + BatchGetItemOperation operation = BatchGetItemOperation.create(emptyRequest()); + DynamoDbAsyncClient mockAsyncClient = mock(DynamoDbAsyncClient.class); + BatchGetItemRequest request = BatchGetItemRequest.builder().build(); + BatchGetItemPublisher mockPublisher = mock(BatchGetItemPublisher.class); + when(mockAsyncClient.batchGetItemPaginator(any(BatchGetItemRequest.class))).thenReturn(mockPublisher); + + Function> asyncCall = operation.asyncServiceCall(mockAsyncClient); + SdkPublisher result = asyncCall.apply(request); + + assertThat(result).isEqualTo(mockPublisher); + verify(mockAsyncClient).batchGetItemPaginator(any(BatchGetItemRequest.class)); } private static BatchGetItemEnhancedRequest emptyRequest() { @@ -425,4 +558,78 @@ private static BatchGetItemResponse generateFakeResults( .build(); } + } + + /** + * {@link Parameterized} cannot share a class-level runner with {@link MockitoJUnitRunner}; this suite lives in the + * outer class alongside the default batch tests via {@link Enclosed}. + */ + @RunWith(Parameterized.class) + public static class IncompatibleConsistentRead { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private DynamoDbClient mockDynamoDbClient; + + private DynamoDbTable fakeItemMappedTable; + + @Parameterized.Parameter(0) + public Boolean firstConsistentRead; + + @Parameterized.Parameter(1) + public Boolean secondConsistentRead; + + @Parameterized.Parameter(2) + public String partitionKey1; + + @Parameterized.Parameter(3) + public String partitionKey2; + + @Parameterized.Parameters(name = "{index}: first={0}, second={1}, keys {2},{3}") + public static Collection parameters() { + return Arrays.asList(new Object[][] { + { true, false, "1", "2" }, + { true, null, "3", "4" }, + { null, false, "5", "6" } + }); + } + + @Before + public void setupMappedTable() { + DynamoDbEnhancedClient enhancedClient = + DynamoDbEnhancedClient.builder().dynamoDbClient(mockDynamoDbClient).extensions().build(); + fakeItemMappedTable = enhancedClient.table(TABLE_NAME, FakeItem.getTableSchema()); + } + + @Test + public void generateRequest_mergesKeysAndAttributes_incompatibleConsistentRead_throws() { + ReadBatch batch1 = readBatchWithOptionalConsistentRead(partitionKey1, firstConsistentRead); + ReadBatch batch2 = readBatchWithOptionalConsistentRead(partitionKey2, secondConsistentRead); + BatchGetItemEnhancedRequest req = BatchGetItemEnhancedRequest.builder() + .readBatches(batch1, batch2) + .build(); + BatchGetItemOperation op = BatchGetItemOperation.create(req); + + assertThatThrownBy(() -> op.generateRequest(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("same 'consistentRead' setting"); + } + + private ReadBatch readBatchWithOptionalConsistentRead(String partitionKey, Boolean consistentRead) { + ReadBatch.Builder builder = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable); + Key key = Key.builder().partitionValue(partitionKey).build(); + if (consistentRead == null) { + builder.addGetItem(key); + } else { + builder.addGetItem(GetItemEnhancedRequest.builder() + .key(key) + .consistentRead(consistentRead) + .build()); + } + return builder.build(); + } + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/BatchWriteItemOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/BatchWriteItemOperationTest.java index 42558ce659af..31a87086d03c 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/BatchWriteItemOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/BatchWriteItemOperationTest.java @@ -15,15 +15,11 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; -import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.stream.Collectors.toList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.sameInstance; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doReturn; @@ -112,6 +108,22 @@ public void setupMappedTables() { FakeItemWithSort.getTableSchema()); } + @Test + public void returnsCorrectOperationName() { + WriteBatch batch = WriteBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addPutItem(r -> r.item(FAKE_ITEMS.get(0))) + .build(); + + BatchWriteItemEnhancedRequest batchWriteItemEnhancedRequest = + BatchWriteItemEnhancedRequest.builder() + .writeBatches(batch) + .build(); + BatchWriteItemOperation operation = BatchWriteItemOperation.create(batchWriteItemEnhancedRequest); + + assertThat(operation.operationName().label()).isEqualTo("BatchWriteItem"); + } + @Test public void getServiceCall_makesTheRightCallAndReturnsResponse() { @@ -142,7 +154,7 @@ public void getServiceCall_makesTheRightCallAndReturnsResponse() { BatchWriteItemResponse response = operation.serviceCall(mockDynamoDbClient).apply(request); - assertThat(response, sameInstance(expectedResponse)); + assertThat(response).isSameAs(expectedResponse); verify(mockDynamoDbClient).batchWriteItem(request); } @@ -171,12 +183,12 @@ public void generateRequest_multipleTables_mixedCommands_usingShortcutForm() { List writeRequests1 = request.requestItems().get(TABLE_NAME); List writeRequests2 = request.requestItems().get(TABLE_NAME_2); - assertThat(writeRequests1, containsInAnyOrder(putRequest(FAKE_ITEM_MAPS.get(0)), + assertThat(writeRequests1).containsExactlyInAnyOrder(putRequest(FAKE_ITEM_MAPS.get(0)), deleteRequest(FAKE_ITEM_MAPS.get(1)), - putRequest(FAKE_ITEM_MAPS.get(2)))); - assertThat(writeRequests2, containsInAnyOrder(deleteRequest(FAKESORT_ITEM_MAPS.get(0)), + putRequest(FAKE_ITEM_MAPS.get(2))); + assertThat(writeRequests2).containsExactlyInAnyOrder(deleteRequest(FAKESORT_ITEM_MAPS.get(0)), putRequest(FAKESORT_ITEM_MAPS.get(1)), - deleteRequest(FAKESORT_ITEM_MAPS.get(2)))); + deleteRequest(FAKESORT_ITEM_MAPS.get(2))); } @Test @@ -204,12 +216,12 @@ public void generateRequest_multipleTables_mixedCommands_usingKeyItemForm() { List writeRequests1 = request.requestItems().get(TABLE_NAME); List writeRequests2 = request.requestItems().get(TABLE_NAME_2); - assertThat(writeRequests1, containsInAnyOrder(putRequest(FAKE_ITEM_MAPS.get(0)), + assertThat(writeRequests1).containsExactlyInAnyOrder(putRequest(FAKE_ITEM_MAPS.get(0)), deleteRequest(FAKE_ITEM_MAPS.get(1)), - putRequest(FAKE_ITEM_MAPS.get(2)))); - assertThat(writeRequests2, containsInAnyOrder(deleteRequest(FAKESORT_ITEM_MAPS.get(0)), + putRequest(FAKE_ITEM_MAPS.get(2))); + assertThat(writeRequests2).containsExactlyInAnyOrder(deleteRequest(FAKESORT_ITEM_MAPS.get(0)), putRequest(FAKESORT_ITEM_MAPS.get(1)), - deleteRequest(FAKESORT_ITEM_MAPS.get(2)))); + deleteRequest(FAKESORT_ITEM_MAPS.get(2))); } @Test @@ -221,15 +233,17 @@ public void generateRequest_multipleTables_extensionOnlyTransformsPutsAndNotDele .when(mockExtension) .beforeWrite( argThat(extensionContext -> - extensionContext.operationContext().tableName().equals(TABLE_NAME) && - extensionContext.items().equals(FAKE_ITEM_MAPS.get(i)) + extensionContext.tableSchema().equals(FakeItem.getTableSchema()) + && extensionContext.operationContext().tableName().equals(TABLE_NAME) + && extensionContext.items().equals(FAKE_ITEM_MAPS.get(i)) )); lenient().doReturn(WriteModification.builder().transformedItem(FAKESORT_ITEM_MAPS.get(i + 3)).build()) .when(mockExtension) .beforeWrite( argThat(extensionContext -> - extensionContext.operationContext().tableName().equals(TABLE_NAME_2) && - extensionContext.items().equals(FAKESORT_ITEM_MAPS.get(i)) + extensionContext.tableSchema().equals(FakeItemWithSort.getTableSchema()) + && extensionContext.operationContext().tableName().equals(TABLE_NAME_2) + && extensionContext.items().equals(FAKESORT_ITEM_MAPS.get(i)) )); }); @@ -258,12 +272,12 @@ public void generateRequest_multipleTables_extensionOnlyTransformsPutsAndNotDele List writeRequests2 = request.requestItems().get(TABLE_NAME_2); // Only PutItem requests should have their attributes transformed - assertThat(writeRequests1, containsInAnyOrder(putRequest(FAKE_ITEM_MAPS.get(3)), + assertThat(writeRequests1).containsExactlyInAnyOrder(putRequest(FAKE_ITEM_MAPS.get(3)), deleteRequest(FAKE_ITEM_MAPS.get(1)), - putRequest(FAKE_ITEM_MAPS.get(5)))); - assertThat(writeRequests2, containsInAnyOrder(deleteRequest(FAKESORT_ITEM_MAPS.get(0)), + putRequest(FAKE_ITEM_MAPS.get(5))); + assertThat(writeRequests2).containsExactlyInAnyOrder(deleteRequest(FAKESORT_ITEM_MAPS.get(0)), putRequest(FAKESORT_ITEM_MAPS.get(4)), - deleteRequest(FAKESORT_ITEM_MAPS.get(2)))); + deleteRequest(FAKESORT_ITEM_MAPS.get(2))); } @Test(expected = IllegalArgumentException.class) @@ -316,14 +330,10 @@ public void transformResults_multipleUnprocessedOperations() { BatchWriteResult results = operation.transformResponse(response, mockExtension); - assertThat(results.unprocessedDeleteItemsForTable(fakeItemMappedTableWithExtension), - containsInAnyOrder(FAKE_ITEM_KEYS.get(1), FAKE_ITEM_KEYS.get(2))); - assertThat(results.unprocessedPutItemsForTable(fakeItemMappedTableWithExtension), - containsInAnyOrder(FAKE_ITEMS.get(0))); - assertThat(results.unprocessedDeleteItemsForTable(fakeItemWithSortMappedTableWithExtension), - containsInAnyOrder(FAKESORT_ITEM_KEYS.get(0))); - assertThat(results.unprocessedPutItemsForTable(fakeItemWithSortMappedTableWithExtension), - containsInAnyOrder(FAKESORT_ITEMS.get(1), FAKESORT_ITEMS.get(2))); + assertThat(results.unprocessedDeleteItemsForTable(fakeItemMappedTableWithExtension)).containsExactlyInAnyOrder(FAKE_ITEM_KEYS.get(1), FAKE_ITEM_KEYS.get(2)); + assertThat(results.unprocessedPutItemsForTable(fakeItemMappedTableWithExtension)).containsExactlyInAnyOrder(FAKE_ITEMS.get(0)); + assertThat(results.unprocessedDeleteItemsForTable(fakeItemWithSortMappedTableWithExtension)).containsExactlyInAnyOrder(FAKESORT_ITEM_KEYS.get(0)); + assertThat(results.unprocessedPutItemsForTable(fakeItemWithSortMappedTableWithExtension)).containsExactlyInAnyOrder(FAKESORT_ITEMS.get(1), FAKESORT_ITEMS.get(2)); } @Test @@ -350,27 +360,25 @@ public void transformResults_multipleUnprocessedOperations_extensionTransformsPu .when(mockExtension) .afterRead( argThat(extensionContext -> - extensionContext.operationContext().tableName().equals(TABLE_NAME) && - extensionContext.items().equals(FAKE_ITEM_MAPS.get(i)) + extensionContext.tableSchema().equals(FakeItem.getTableSchema()) + && extensionContext.operationContext().tableName().equals(TABLE_NAME) + && extensionContext.items().equals(FAKE_ITEM_MAPS.get(i)) )); doReturn(ReadModification.builder().transformedItem(FAKESORT_ITEM_MAPS.get(i + 3)).build()) .when(mockExtension) .afterRead(argThat(extensionContext -> - extensionContext.operationContext().tableName().equals(TABLE_NAME_2) && - extensionContext.items().equals(FAKESORT_ITEM_MAPS.get(i)) + extensionContext.tableSchema().equals(FakeItemWithSort.getTableSchema()) + && extensionContext.operationContext().tableName().equals(TABLE_NAME_2) + && extensionContext.items().equals(FAKESORT_ITEM_MAPS.get(i)) )); }); BatchWriteResult results = operation.transformResponse(response, mockExtension); - assertThat(results.unprocessedDeleteItemsForTable(fakeItemMappedTableWithExtension), - containsInAnyOrder(FAKE_ITEM_KEYS.get(1), FAKE_ITEM_KEYS.get(2))); - assertThat(results.unprocessedPutItemsForTable(fakeItemMappedTableWithExtension), - containsInAnyOrder(FAKE_ITEMS.get(3))); - assertThat(results.unprocessedDeleteItemsForTable(fakeItemWithSortMappedTableWithExtension), - containsInAnyOrder(FAKESORT_ITEM_KEYS.get(0))); - assertThat(results.unprocessedPutItemsForTable(fakeItemWithSortMappedTableWithExtension), - containsInAnyOrder(FAKESORT_ITEMS.get(4), FAKESORT_ITEMS.get(5))); + assertThat(results.unprocessedDeleteItemsForTable(fakeItemMappedTableWithExtension)).containsExactlyInAnyOrder(FAKE_ITEM_KEYS.get(1), FAKE_ITEM_KEYS.get(2)); + assertThat(results.unprocessedPutItemsForTable(fakeItemMappedTableWithExtension)).containsExactlyInAnyOrder(FAKE_ITEMS.get(3)); + assertThat(results.unprocessedDeleteItemsForTable(fakeItemWithSortMappedTableWithExtension)).containsExactlyInAnyOrder(FAKESORT_ITEM_KEYS.get(0)); + assertThat(results.unprocessedPutItemsForTable(fakeItemWithSortMappedTableWithExtension)).containsExactlyInAnyOrder(FAKESORT_ITEMS.get(4), FAKESORT_ITEMS.get(5)); } @Test @@ -384,8 +392,8 @@ public void transformResults_noUnprocessedOperations() { BatchWriteResult results = operation.transformResponse(response, mockExtension); - assertThat(results.unprocessedDeleteItemsForTable(fakeItemMappedTable), is(emptyList())); - assertThat(results.unprocessedPutItemsForTable(fakeItemMappedTable), is(emptyList())); + assertThat(results.unprocessedDeleteItemsForTable(fakeItemMappedTable)).isEmpty(); + assertThat(results.unprocessedPutItemsForTable(fakeItemMappedTable)).isEmpty(); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/CreateTableOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/CreateTableOperationTest.java index b058314279c7..27ade077fba9 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/CreateTableOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/CreateTableOperationTest.java @@ -15,12 +15,8 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.sameInstance; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.verify; @@ -34,6 +30,7 @@ import java.util.Set; import java.util.stream.Collectors; import org.hamcrest.Description; +import org.hamcrest.MatcherAssert; import org.hamcrest.TypeSafeMatcher; import org.junit.Test; import org.junit.runner.RunWith; @@ -117,6 +114,14 @@ public void describeTo(Description description) { } } + @Test + public void returnsCorrectOperationName() { + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder().build()); + + assertThat(operation.operationName().label()).isEqualTo("CreateTable"); + } + @Test public void generateRequest_withLsiAndGsi() { Projection projection1 = Projection.builder().projectionType(ProjectionType.ALL).build(); @@ -160,15 +165,15 @@ public void generateRequest_withLsiAndGsi() { - assertThat(request.tableName(), is(TABLE_NAME)); - assertThat(request.keySchema(), containsInAnyOrder(KeySchemaElement.builder() + assertThat(request.tableName()).isEqualTo(TABLE_NAME); + assertThat(request.keySchema()).containsExactlyInAnyOrder(KeySchemaElement.builder() .attributeName("id") .keyType(HASH) .build(), KeySchemaElement.builder() .attributeName("sort") .keyType(RANGE) - .build())); + .build()); software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex expectedGsi1 = software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex.builder() .indexName("gsi_1") @@ -193,8 +198,8 @@ public void generateRequest_withLsiAndGsi() { .projection(projection2) .provisionedThroughput(provisionedThroughput2) .build(); - assertThat(request.globalSecondaryIndexes(), containsInAnyOrder(matchesGsi(expectedGsi1), - matchesGsi(expectedGsi2))); + MatcherAssert.assertThat(request.globalSecondaryIndexes(), containsInAnyOrder(matchesGsi(expectedGsi1), + matchesGsi(expectedGsi2))); software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex expectedLsi = software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex.builder() .indexName("lsi_1") @@ -208,8 +213,8 @@ public void generateRequest_withLsiAndGsi() { .build()) .projection(projection3) .build(); - assertThat(request.localSecondaryIndexes(), containsInAnyOrder(expectedLsi)); - assertThat(request.attributeDefinitions(), containsInAnyOrder( + assertThat(request.localSecondaryIndexes()).containsExactlyInAnyOrder(expectedLsi); + assertThat(request.attributeDefinitions()).containsExactlyInAnyOrder( AttributeDefinition.builder() .attributeName("id") .attributeType(ScalarAttributeType.S) @@ -229,7 +234,7 @@ public void generateRequest_withLsiAndGsi() { AttributeDefinition.builder() .attributeName("gsi_sort") .attributeType(ScalarAttributeType.S) - .build())); + .build()); } @Test(expected = IllegalArgumentException.class) @@ -277,11 +282,11 @@ public void generateRequest_validLsiAsGsiReference() { CreateTableRequest request = operation.generateRequest(FakeItemWithIndices.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(1)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(1); software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex globalSecondaryIndex = request.globalSecondaryIndexes().get(0); - assertThat(globalSecondaryIndex.indexName(), is("lsi_1")); + assertThat(globalSecondaryIndex.indexName()).isEqualTo("lsi_1"); } @Test @@ -300,7 +305,7 @@ public void generateRequest_nonReferencedIndicesDoNotCreateExtraAttributeDefinit .attributeType(ScalarAttributeType.S) .build(); - assertThat(request.attributeDefinitions(), containsInAnyOrder(attributeDefinition1, attributeDefinition2)); + assertThat(request.attributeDefinitions()).containsExactlyInAnyOrder(attributeDefinition1, attributeDefinition2); } @Test(expected = IllegalArgumentException.class) @@ -328,8 +333,8 @@ public void generateRequest_withProvisionedThroughput() { PRIMARY_CONTEXT, null); - assertThat(request.billingMode(), is(BillingMode.PROVISIONED)); - assertThat(request.provisionedThroughput(), is(provisionedThroughput)); + assertThat(request.billingMode()).isEqualTo(BillingMode.PROVISIONED); + assertThat(request.provisionedThroughput()).isEqualTo(provisionedThroughput); } @Test @@ -340,7 +345,7 @@ public void generateRequest_withNoProvisionedThroughput() { PRIMARY_CONTEXT, null); - assertThat(request.billingMode(), is(BillingMode.PAY_PER_REQUEST)); + assertThat(request.billingMode()).isEqualTo(BillingMode.PAY_PER_REQUEST); } @Test @@ -357,7 +362,7 @@ public void generateRequest_withStreamSpecification() { PRIMARY_CONTEXT, null); - assertThat(request.streamSpecification(), is(streamSpecification)); + assertThat(request.streamSpecification()).isEqualTo(streamSpecification); } @Test @@ -368,7 +373,7 @@ public void generateRequest_withNoStreamSpecification() { PRIMARY_CONTEXT, null); - assertThat(request.streamSpecification(), is(nullValue())); + assertThat(request.streamSpecification()).isNull(); } @@ -381,20 +386,20 @@ public void generateRequest_withNumericKey() { PRIMARY_CONTEXT, null); - assertThat(request.tableName(), is(TABLE_NAME)); - assertThat(request.keySchema(), containsInAnyOrder(KeySchemaElement.builder() + assertThat(request.tableName()).isEqualTo(TABLE_NAME); + assertThat(request.keySchema()).containsExactlyInAnyOrder(KeySchemaElement.builder() .attributeName("id") .keyType(HASH) .build(), KeySchemaElement.builder() .attributeName("sort") .keyType(RANGE) - .build())); + .build()); - assertThat(request.globalSecondaryIndexes(), is(DefaultSdkAutoConstructList.getInstance())); - assertThat(request.localSecondaryIndexes(), is(DefaultSdkAutoConstructList.getInstance())); + assertThat(request.globalSecondaryIndexes()).isEqualTo(DefaultSdkAutoConstructList.getInstance()); + assertThat(request.localSecondaryIndexes()).isEqualTo(DefaultSdkAutoConstructList.getInstance()); - assertThat(request.attributeDefinitions(), containsInAnyOrder( + assertThat(request.attributeDefinitions()).containsExactlyInAnyOrder( AttributeDefinition.builder() .attributeName("id") .attributeType(ScalarAttributeType.S) @@ -402,7 +407,7 @@ public void generateRequest_withNumericKey() { AttributeDefinition.builder() .attributeName("sort") .attributeType(ScalarAttributeType.N) - .build())); + .build()); } @Test @@ -414,20 +419,20 @@ public void generateRequest_withBinaryKey() { PRIMARY_CONTEXT, null); - assertThat(request.tableName(), is(TABLE_NAME)); - assertThat(request.keySchema(), containsInAnyOrder(KeySchemaElement.builder() + assertThat(request.tableName()).isEqualTo(TABLE_NAME); + assertThat(request.keySchema()).containsExactlyInAnyOrder(KeySchemaElement.builder() .attributeName("id") .keyType(HASH) - .build())); + .build()); - assertThat(request.globalSecondaryIndexes(), is(empty())); - assertThat(request.localSecondaryIndexes(), is(empty())); + assertThat(request.globalSecondaryIndexes()).isEmpty(); + assertThat(request.localSecondaryIndexes()).isEmpty(); - assertThat(request.attributeDefinitions(), containsInAnyOrder( + assertThat(request.attributeDefinitions()).containsExactlyInAnyOrder( AttributeDefinition.builder() .attributeName("id") .attributeType(ScalarAttributeType.B) - .build())); + .build()); } @Test @@ -439,20 +444,20 @@ public void generateRequest_withByteBufferKey() { PRIMARY_CONTEXT, null); - assertThat(request.tableName(), is(TABLE_NAME)); - assertThat(request.keySchema(), containsInAnyOrder(KeySchemaElement.builder() + assertThat(request.tableName()).isEqualTo(TABLE_NAME); + assertThat(request.keySchema()).containsExactlyInAnyOrder(KeySchemaElement.builder() .attributeName("id") .keyType(HASH) - .build())); + .build()); - assertThat(request.globalSecondaryIndexes(), is(empty())); - assertThat(request.localSecondaryIndexes(), is(empty())); + assertThat(request.globalSecondaryIndexes()).isEmpty(); + assertThat(request.localSecondaryIndexes()).isEmpty(); - assertThat(request.attributeDefinitions(), containsInAnyOrder( + assertThat(request.attributeDefinitions()).containsExactlyInAnyOrder( AttributeDefinition.builder() .attributeName("id") .attributeType(ScalarAttributeType.B) - .build())); + .build()); } @Test(expected = IllegalArgumentException.class) @@ -472,7 +477,7 @@ public void getServiceCall_makesTheRightCallAndReturnsResponse() { CreateTableResponse actualResponse = operation.serviceCall(mockDynamoDbClient).apply(createTableRequest); - assertThat(actualResponse, sameInstance(expectedResponse)); + assertThat(actualResponse).isSameAs(expectedResponse); verify(mockDynamoDbClient).createTable(same(createTableRequest)); } @@ -501,10 +506,10 @@ public void generateRequest_gsiWithSingleKeys_buildsCorrectly() { CreateTableRequest request = operation.generateRequest(FakeItemWithIndices.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(1)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(1); GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); - assertThat(gsi.indexName(), is("gsi_1")); - assertThat(gsi.keySchema().size(), is(2)); + assertThat(gsi.indexName()).isEqualTo("gsi_1"); + assertThat(gsi.keySchema().size()).isEqualTo(2); } @Test @@ -524,23 +529,23 @@ public void generateRequest_gsiWithCompositeKeys() { CreateTableRequest request = operation.generateRequest(FakeItemWithCompositeGsi.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(1)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(1); GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); - assertThat(gsi.indexName(), is("composite_gsi")); - assertThat(gsi.keySchema().size(), is(4)); + assertThat(gsi.indexName()).isEqualTo("composite_gsi"); + assertThat(gsi.keySchema().size()).isEqualTo(4); Set partitionKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == HASH) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(partitionKeyNames, containsInAnyOrder("gsi_pk1", "gsi_pk2")); + assertThat(partitionKeyNames).containsExactlyInAnyOrder("gsi_pk1", "gsi_pk2"); Set sortKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == RANGE) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(sortKeyNames, containsInAnyOrder("gsi_sk1", "gsi_sk2")); + assertThat(sortKeyNames).containsExactlyInAnyOrder("gsi_sk1", "gsi_sk2"); } @Test @@ -560,12 +565,12 @@ public void generateRequest_gsiWithFlattenedPartitionKey() { CreateTableRequest request = operation.generateRequest(FakeItemWithFlattenedGsi.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(1)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(1); GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); - assertThat(gsi.indexName(), is("flatten_partition_gsi")); - assertThat(gsi.keySchema().size(), is(1)); - assertThat(gsi.keySchema().get(0).attributeName(), is("gsiPartitionKey")); - assertThat(gsi.keySchema().get(0).keyType(), is(HASH)); + assertThat(gsi.indexName()).isEqualTo("flatten_partition_gsi"); + assertThat(gsi.keySchema().size()).isEqualTo(1); + assertThat(gsi.keySchema().get(0).attributeName()).isEqualTo("gsiPartitionKey"); + assertThat(gsi.keySchema().get(0).keyType()).isEqualTo(HASH); } @Test @@ -585,14 +590,14 @@ public void generateRequest_gsiWithFlattenedSortKey() { CreateTableRequest request = operation.generateRequest(FakeItemWithFlattenedGsi.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(1)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(1); GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); - assertThat(gsi.indexName(), is("flatten_sort_gsi")); - assertThat(gsi.keySchema().size(), is(2)); - assertThat(gsi.keySchema().get(0).attributeName(), is("id")); - assertThat(gsi.keySchema().get(0).keyType(), is(HASH)); - assertThat(gsi.keySchema().get(1).attributeName(), is("gsiSortKey")); - assertThat(gsi.keySchema().get(1).keyType(), is(RANGE)); + assertThat(gsi.indexName()).isEqualTo("flatten_sort_gsi"); + assertThat(gsi.keySchema().size()).isEqualTo(2); + assertThat(gsi.keySchema().get(0).attributeName()).isEqualTo("id"); + assertThat(gsi.keySchema().get(0).keyType()).isEqualTo(HASH); + assertThat(gsi.keySchema().get(1).attributeName()).isEqualTo("gsiSortKey"); + assertThat(gsi.keySchema().get(1).keyType()).isEqualTo(RANGE); } @Test @@ -612,22 +617,22 @@ public void generateRequest_gsiWithMixedFlattenedKeys() { CreateTableRequest request = operation.generateRequest(FakeItemWithFlattenedGsi.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(1)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(1); GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); - assertThat(gsi.indexName(), is("flatten_mixed_gsi")); - assertThat(gsi.keySchema().size(), is(2)); + assertThat(gsi.indexName()).isEqualTo("flatten_mixed_gsi"); + assertThat(gsi.keySchema().size()).isEqualTo(2); Set partitionKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == HASH) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(partitionKeyNames, containsInAnyOrder("gsiMixedPartitionKey")); + assertThat(partitionKeyNames).containsExactlyInAnyOrder("gsiMixedPartitionKey"); Set sortKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == RANGE) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(sortKeyNames, containsInAnyOrder("gsiMixedSortKey")); + assertThat(sortKeyNames).containsExactlyInAnyOrder("gsiMixedSortKey"); } @Test @@ -647,22 +652,22 @@ public void generateRequest_gsiWithBothFlattenedKeys() { CreateTableRequest request = operation.generateRequest(FakeItemWithFlattenedGsi.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(1)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(1); GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); - assertThat(gsi.indexName(), is("flatten_both_gsi")); - assertThat(gsi.keySchema().size(), is(2)); + assertThat(gsi.indexName()).isEqualTo("flatten_both_gsi"); + assertThat(gsi.keySchema().size()).isEqualTo(2); Set partitionKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == HASH) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(partitionKeyNames, containsInAnyOrder("gsiBothSortKey")); + assertThat(partitionKeyNames).containsExactlyInAnyOrder("gsiBothSortKey"); Set sortKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == RANGE) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(sortKeyNames, containsInAnyOrder("gsiBothSortKey")); + assertThat(sortKeyNames).containsExactlyInAnyOrder("gsiBothSortKey"); } @Test @@ -682,16 +687,16 @@ public void generateRequest_gsiWithMixedCompositePartitionKeys() { CreateTableRequest request = operation.generateRequest(FakeItemWithMixedCompositeGsi.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(1)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(1); GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); - assertThat(gsi.indexName(), is("mixed_partition_gsi")); - assertThat(gsi.keySchema().size(), is(4)); + assertThat(gsi.indexName()).isEqualTo("mixed_partition_gsi"); + assertThat(gsi.keySchema().size()).isEqualTo(4); Set partitionKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == HASH) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(partitionKeyNames, containsInAnyOrder("rootPartitionKey1", "rootPartitionKey2", "flattenedPartitionKey1", "flattenedPartitionKey2")); + assertThat(partitionKeyNames).containsExactlyInAnyOrder("rootPartitionKey1", "rootPartitionKey2", "flattenedPartitionKey1", "flattenedPartitionKey2"); } @Test @@ -711,22 +716,22 @@ public void generateRequest_gsiWithMixedCompositeSortKeys() { CreateTableRequest request = operation.generateRequest(FakeItemWithMixedCompositeGsi.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(1)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(1); GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); - assertThat(gsi.indexName(), is("mixed_sort_gsi")); - assertThat(gsi.keySchema().size(), is(6)); + assertThat(gsi.indexName()).isEqualTo("mixed_sort_gsi"); + assertThat(gsi.keySchema().size()).isEqualTo(6); Set partitionKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == HASH) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(partitionKeyNames, containsInAnyOrder("rootPartitionKey1", "rootPartitionKey2")); + assertThat(partitionKeyNames).containsExactlyInAnyOrder("rootPartitionKey1", "rootPartitionKey2"); Set sortKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == RANGE) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(sortKeyNames, containsInAnyOrder("rootSortKey1", "rootSortKey2", "flattenedSortKey1", "flattenedSortKey2")); + assertThat(sortKeyNames).containsExactlyInAnyOrder("rootSortKey1", "rootSortKey2", "flattenedSortKey1", "flattenedSortKey2"); } @Test @@ -746,22 +751,22 @@ public void generateRequest_gsiWithFullMixedCompositeKeys() { CreateTableRequest request = operation.generateRequest(FakeItemWithMixedCompositeGsi.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(1)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(1); GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); - assertThat(gsi.indexName(), is("full_mixed_gsi")); - assertThat(gsi.keySchema().size(), is(8)); + assertThat(gsi.indexName()).isEqualTo("full_mixed_gsi"); + assertThat(gsi.keySchema().size()).isEqualTo(8); Set partitionKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == HASH) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(partitionKeyNames, containsInAnyOrder("rootPartitionKey1", "rootPartitionKey2", "flattenedPartitionKey1", "flattenedPartitionKey2")); + assertThat(partitionKeyNames).containsExactlyInAnyOrder("rootPartitionKey1", "rootPartitionKey2", "flattenedPartitionKey1", "flattenedPartitionKey2"); Set sortKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == RANGE) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(sortKeyNames, containsInAnyOrder("rootSortKey1", "rootSortKey2", "flattenedSortKey1", "flattenedSortKey2")); + assertThat(sortKeyNames).containsExactlyInAnyOrder("rootSortKey1", "rootSortKey2", "flattenedSortKey1", "flattenedSortKey2"); } @Test @@ -781,22 +786,22 @@ public void generateRequest_immutableGsiWithCompositeKeys() { CreateTableRequest request = operation.generateRequest(ImmutableTableSchema.create(CompositeMetadataImmutable.class), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(1)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(1); GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); - assertThat(gsi.indexName(), is("gsi1")); - assertThat(gsi.keySchema().size(), is(4)); + assertThat(gsi.indexName()).isEqualTo("gsi1"); + assertThat(gsi.keySchema().size()).isEqualTo(4); Set partitionKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == HASH) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(partitionKeyNames, containsInAnyOrder("gsiPk1", "gsiPk2")); + assertThat(partitionKeyNames).containsExactlyInAnyOrder("gsiPk1", "gsiPk2"); Set sortKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == RANGE) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(sortKeyNames, containsInAnyOrder("gsiSk1", "gsiSk2")); + assertThat(sortKeyNames).containsExactlyInAnyOrder("gsiSk1", "gsiSk2"); } @Test @@ -821,25 +826,25 @@ public void generateRequest_immutableGsiWithCrossIndexKeys() { CreateTableRequest request = operation.generateRequest(ImmutableTableSchema.create(CrossIndexImmutable.class), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(2)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(2); GlobalSecondaryIndex gsi1 = request.globalSecondaryIndexes().stream() .filter(gsi -> "gsi1".equals(gsi.indexName())) .findFirst().orElse(null); - assertThat(gsi1.keySchema().size(), is(2)); - assertThat(gsi1.keySchema().get(0).attributeName(), is("attr1")); - assertThat(gsi1.keySchema().get(0).keyType(), is(HASH)); - assertThat(gsi1.keySchema().get(1).attributeName(), is("attr2")); - assertThat(gsi1.keySchema().get(1).keyType(), is(HASH)); + assertThat(gsi1.keySchema().size()).isEqualTo(2); + assertThat(gsi1.keySchema().get(0).attributeName()).isEqualTo("attr1"); + assertThat(gsi1.keySchema().get(0).keyType()).isEqualTo(HASH); + assertThat(gsi1.keySchema().get(1).attributeName()).isEqualTo("attr2"); + assertThat(gsi1.keySchema().get(1).keyType()).isEqualTo(HASH); GlobalSecondaryIndex gsi2 = request.globalSecondaryIndexes().stream() .filter(gsi -> "gsi2".equals(gsi.indexName())) .findFirst().orElse(null); - assertThat(gsi2.keySchema().size(), is(2)); - assertThat(gsi2.keySchema().get(0).attributeName(), is("attr3")); - assertThat(gsi2.keySchema().get(0).keyType(), is(HASH)); - assertThat(gsi2.keySchema().get(1).attributeName(), is("attr1")); - assertThat(gsi2.keySchema().get(1).keyType(), is(RANGE)); + assertThat(gsi2.keySchema().size()).isEqualTo(2); + assertThat(gsi2.keySchema().get(0).attributeName()).isEqualTo("attr3"); + assertThat(gsi2.keySchema().get(0).keyType()).isEqualTo(HASH); + assertThat(gsi2.keySchema().get(1).attributeName()).isEqualTo("attr1"); + assertThat(gsi2.keySchema().get(1).keyType()).isEqualTo(RANGE); } @Test @@ -859,21 +864,21 @@ public void generateRequest_immutableGsiWithMixedFlattenedKeys() { CreateTableRequest request = operation.generateRequest(ImmutableTableSchema.create(MixedFlattenedImmutable.class), PRIMARY_CONTEXT, null); - assertThat(request.globalSecondaryIndexes().size(), is(1)); + assertThat(request.globalSecondaryIndexes().size()).isEqualTo(1); GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); - assertThat(gsi.indexName(), is("mixed_gsi")); - assertThat(gsi.keySchema().size(), is(4)); + assertThat(gsi.indexName()).isEqualTo("mixed_gsi"); + assertThat(gsi.keySchema().size()).isEqualTo(4); Set partitionKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == HASH) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(partitionKeyNames, containsInAnyOrder("rootKey1", "flatKey1")); + assertThat(partitionKeyNames).containsExactlyInAnyOrder("rootKey1", "flatKey1"); Set sortKeyNames = gsi.keySchema().stream() .filter(key -> key.keyType() == RANGE) .map(KeySchemaElement::attributeName) .collect(Collectors.toSet()); - assertThat(sortKeyNames, containsInAnyOrder("rootKey2", "flatKey2")); + assertThat(sortKeyNames).containsExactlyInAnyOrder("rootKey2", "flatKey2"); } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/DeleteItemOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/DeleteItemOperationTest.java index c8a2ab5fb7f9..a450f497a970 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/DeleteItemOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/DeleteItemOperationTest.java @@ -15,12 +15,8 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.sameInstance; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; @@ -93,6 +89,19 @@ public class DeleteItemOperationTest { @Mock private DynamoDbEnhancedClientExtension mockDynamoDbEnhancedClientExtension; + @Test + public void returnsCorrectOperationName() { + FakeItem keyItem = createUniqueFakeItem(); + ReturnConsumedCapacity returnConsumedCapacity = ReturnConsumedCapacity.TOTAL; + DeleteItemOperation operation = + DeleteItemOperation.create(DeleteItemEnhancedRequest.builder() + .key(k -> k.partitionValue(keyItem.getId())) + .returnConsumedCapacity(returnConsumedCapacity) + .build()); + + assertThat(operation.operationName().label()).isEqualTo("DeleteItem"); + } + @Test public void getServiceCall_makesTheRightCallAndReturnsResponse() { FakeItem keyItem = createUniqueFakeItem(); @@ -104,7 +113,7 @@ public void getServiceCall_makesTheRightCallAndReturnsResponse() { DeleteItemResponse response = deleteItemOperation.serviceCall(mockDynamoDbClient).apply(deleteItemRequest); - assertThat(response, sameInstance(expectedResponse)); + assertThat(response).isSameAs(expectedResponse); verify(mockDynamoDbClient).deleteItem(deleteItemRequest); } @@ -132,7 +141,7 @@ public void generateRequest_withReturnConsumedCapacity_unknownValue_generatesCor .returnValues(ReturnValue.ALL_OLD) .returnConsumedCapacity(returnConsumedCapacity) .build(); - assertThat(request, is(expectedRequest)); + assertThat(request).isEqualTo(expectedRequest); } @Test @@ -159,7 +168,7 @@ public void generateRequest_withReturnConsumedCapacity_knownValue_generatesCorre .returnValues(ReturnValue.ALL_OLD) .returnConsumedCapacity(returnConsumedCapacity) .build(); - assertThat(request, is(expectedRequest)); + assertThat(request).isEqualTo(expectedRequest); } @Test @@ -186,7 +195,7 @@ public void generateRequest_withReturnItemCollectionMetrics_unknownValue_generat .returnValues(ReturnValue.ALL_OLD) .returnItemCollectionMetrics(returnItemCollectionMetrics) .build(); - assertThat(request, is(expectedRequest)); + assertThat(request).isEqualTo(expectedRequest); } @Test @@ -213,7 +222,7 @@ public void generateRequest_withReturnItemCollectionMetrics_knownValue_generates .returnValues(ReturnValue.ALL_OLD) .returnItemCollectionMetrics(returnItemCollectionMetrics) .build(); - assertThat(request, is(expectedRequest)); + assertThat(request).isEqualTo(expectedRequest); } @Test @@ -240,7 +249,7 @@ public void generateRequest_withReturnValuesOnConditionCheckFailure_unknownValue .returnValues(ReturnValue.ALL_OLD) .returnValuesOnConditionCheckFailure(returnValuesOnConditionCheckFailure) .build(); - assertThat(request, is(expectedRequest)); + assertThat(request).isEqualTo(expectedRequest); } @Test @@ -267,7 +276,7 @@ public void generateRequest_withReturnValuesOnConditionCheckFailure_knownValue_g .returnValues(ReturnValue.ALL_OLD) .returnValuesOnConditionCheckFailure(returnValuesOnConditionCheckFailure) .build(); - assertThat(request, is(expectedRequest)); + assertThat(request).isEqualTo(expectedRequest); } @Test @@ -290,7 +299,7 @@ public void generateRequest_partitionAndSortKey() { .key(expectedKeyMap) .returnValues(ReturnValue.ALL_OLD) .build(); - assertThat(request, is(expectedRequest)); + assertThat(request).isEqualTo(expectedRequest); } @Test @@ -306,9 +315,9 @@ public void generateRequest_withConditionExpression() { PRIMARY_CONTEXT, null); - assertThat(request.conditionExpression(), is(CONDITION_EXPRESSION.expression())); - assertThat(request.expressionAttributeNames(), is(CONDITION_EXPRESSION.expressionNames())); - assertThat(request.expressionAttributeValues(), is(CONDITION_EXPRESSION.expressionValues())); + assertThat(request.conditionExpression()).isEqualTo(CONDITION_EXPRESSION.expression()); + assertThat(request.expressionAttributeNames()).isEqualTo(CONDITION_EXPRESSION.expressionNames()); + assertThat(request.expressionAttributeValues()).isEqualTo(CONDITION_EXPRESSION.expressionValues()); } @Test @@ -324,9 +333,9 @@ public void generateRequest_withMinimalConditionExpression() { PRIMARY_CONTEXT, null); - assertThat(request.conditionExpression(), is(MINIMAL_CONDITION_EXPRESSION.expression())); - assertThat(request.expressionAttributeNames(), is(emptyMap())); - assertThat(request.expressionAttributeValues(), is(emptyMap())); + assertThat(request.conditionExpression()).isEqualTo(MINIMAL_CONDITION_EXPRESSION.expression()); + assertThat(request.expressionAttributeNames()).isEmpty(); + assertThat(request.expressionAttributeValues()).isEmpty(); } @Test(expected = IllegalArgumentException.class) @@ -362,7 +371,7 @@ public void generateRequest_partitionKeyOnly() { .key(expectedKeyMap) .returnValues(ReturnValue.ALL_OLD) .build(); - assertThat(request, is(expectedRequest)); + assertThat(request).isEqualTo(expectedRequest); } @Test @@ -383,8 +392,8 @@ public void transformResponse_correctlyTransformsIntoAnItem() { null) .attributes(); - assertThat(result.getId(), is(keyItem.getId())); - assertThat(result.getSubclassAttribute(), is("test-value")); + assertThat(result.getId()).isEqualTo(keyItem.getId()); + assertThat(result.getSubclassAttribute()).isEqualTo("test-value"); } @Test @@ -401,7 +410,7 @@ public void transformResponse_noResults_returnsNull() { null) .attributes(); - assertThat(result, is(nullValue())); + assertThat(result).isNull(); } @Test @@ -418,7 +427,7 @@ public void generateRequest_withExtension_doesNotModifyKey() { PRIMARY_CONTEXT, mockDynamoDbEnhancedClientExtension); - assertThat(request.key(), is(keyMap)); + assertThat(request.key()).isEqualTo(keyMap); verify(mockDynamoDbEnhancedClientExtension, never()).beforeWrite(any(DynamoDbExtensionContext.BeforeWrite.class)); } @@ -445,7 +454,7 @@ public void transformResponse_withExtension_appliesItemModification() { mockDynamoDbEnhancedClientExtension) .attributes(); - assertThat(resultItem, is(fakeItem)); + assertThat(resultItem).isEqualTo(fakeItem); verify(mockDynamoDbEnhancedClientExtension).afterRead(DefaultDynamoDbExtensionContext.builder() .tableMetadata(FakeItem.getTableMetadata()) .operationContext(PRIMARY_CONTEXT) @@ -479,7 +488,7 @@ public void generateTransactWriteItem_basicRequest() { .tableName(TABLE_NAME) .build()) .build(); - assertThat(actualResult, is(expectedResult)); + assertThat(actualResult).isEqualTo(expectedResult); verify(deleteItemOperation).generateRequest(FakeItem.getTableSchema(), context, mockDynamoDbEnhancedClientExtension); } @@ -519,7 +528,7 @@ public void generateTransactWriteItem_conditionalRequest() { .expressionAttributeValues(attributeValues) .build()) .build(); - assertThat(actualResult, is(expectedResult)); + assertThat(actualResult).isEqualTo(expectedResult); verify(deleteItemOperation).generateRequest(FakeItem.getTableSchema(), context, mockDynamoDbEnhancedClientExtension); } @@ -553,7 +562,7 @@ public void generateTransactWriteItem_returnValuesOnConditionCheckFailure_genera .returnValuesOnConditionCheckFailure(returnValues) .build()) .build(); - assertThat(actualResult, is(expectedResult)); + assertThat(actualResult).isEqualTo(expectedResult); verify(deleteItemOperation).generateRequest(FakeItem.getTableSchema(), context, mockDynamoDbEnhancedClientExtension); } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/DescribeTableOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/DescribeTableOperationTest.java index 7c80b88cf0af..fa554f8efb31 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/DescribeTableOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/DescribeTableOperationTest.java @@ -15,9 +15,8 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.verify; @@ -46,6 +45,12 @@ public class DescribeTableOperationTest { @Mock private DynamoDbClient mockDynamoDbClient; + @Test + public void returnsCorrectOperationName() { + DescribeTableOperation operation = DescribeTableOperation.create(); + assertThat(operation.operationName().label()).isEqualTo("DescribeTable"); + } + @Test public void getServiceCall_makesTheRightCall() { DescribeTableOperation operation = DescribeTableOperation.create(); @@ -62,7 +67,7 @@ public void generateRequest_from_DescribeTableOperation() { .generateRequest(FakeItemWithSort.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(describeTableRequest, is(describeTableRequest.builder().tableName(TABLE_NAME).build())); + assertThat(describeTableRequest).isEqualTo(DescribeTableRequest.builder().tableName(TABLE_NAME).build()); } @Test diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/GetItemOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/GetItemOperationTest.java index ec213dfe6852..a0295d705180 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/GetItemOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/GetItemOperationTest.java @@ -15,11 +15,8 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; +import static org.assertj.core.api.Assertions.assertThat; import static java.util.Collections.singletonList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; @@ -67,6 +64,17 @@ public class GetItemOperationTest { @Mock private DynamoDbEnhancedClientExtension mockDynamoDbEnhancedClientExtension; + @Test + public void returnsCorrectOperationName() { + FakeItem keyItem = createUniqueFakeItem(); + GetItemOperation operation = + GetItemOperation.create(GetItemEnhancedRequest.builder() + .key(k -> k.partitionValue(keyItem.getId())) + .consistentRead(true).build()); + + assertThat(operation.operationName().label()).isEqualTo("GetItem"); + } + @Test public void getServiceCall_makesTheRightCallAndReturnsResponse() { FakeItem keyItem = createUniqueFakeItem(); @@ -78,7 +86,7 @@ public void getServiceCall_makesTheRightCallAndReturnsResponse() { GetItemResponse response = getItemOperation.serviceCall(mockDynamoDbClient).apply(getItemRequest); - assertThat(response, sameInstance(expectedResponse)); + assertThat(response).isSameAs(expectedResponse); verify(mockDynamoDbClient).getItem(getItemRequest); } @@ -110,7 +118,7 @@ public void generateRequest_consistentRead() { .key(expectedKeyMap) .consistentRead(true) .build(); - assertThat(request, is(expectedRequest)); + assertThat(request).isEqualTo(expectedRequest); } @Test @@ -129,7 +137,7 @@ public void generateRequest_partitionKeyOnly() { .tableName(TABLE_NAME) .key(expectedKeyMap) .build(); - assertThat(request, is(expectedRequest)); + assertThat(request).isEqualTo(expectedRequest); } @Test @@ -152,7 +160,7 @@ public void generateRequest_partitionAndSortKey() { .tableName(TABLE_NAME) .key(expectedKeyMap) .build(); - assertThat(request, is(expectedRequest)); + assertThat(request).isEqualTo(expectedRequest); } @Test @@ -204,7 +212,7 @@ public void transformResponse_noItem() { GetItemEnhancedResponse result = getItemOperation.transformResponse(response, FakeItem.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(result.attributes(), is(nullValue())); + assertThat(result.attributes()).isNull(); } @Test @@ -222,8 +230,8 @@ public void transformResponse_correctlyTransformsIntoAnItem() { GetItemEnhancedResponse result = getItemOperation.transformResponse(response, FakeItem.getTableSchema(), PRIMARY_CONTEXT, null); - assertThat(result.attributes().getId(), is(keyItem.getId())); - assertThat(result.attributes().getSubclassAttribute(), is("test-value")); + assertThat(result.attributes().getId()).isEqualTo(keyItem.getId()); + assertThat(result.attributes().getSubclassAttribute()).isEqualTo("test-value"); } @Test @@ -237,7 +245,7 @@ public void generateRequest_withExtension_doesNotModifyKey() { PRIMARY_CONTEXT, mockDynamoDbEnhancedClientExtension); - assertThat(request.key(), is(keyMap)); + assertThat(request.key()).isEqualTo(keyMap); verify(mockDynamoDbEnhancedClientExtension, never()).beforeWrite(any(DynamoDbExtensionContext.BeforeWrite.class)); } @@ -258,7 +266,7 @@ public void transformResponse_withExtension_appliesItemModification() { GetItemEnhancedResponse resultItem = getItemOperation.transformResponse(response, FakeItem.getTableSchema(), PRIMARY_CONTEXT, mockDynamoDbEnhancedClientExtension); - assertThat(resultItem.attributes(), is(fakeItem)); + assertThat(resultItem.attributes()).isEqualTo(fakeItem); verify(mockDynamoDbEnhancedClientExtension).afterRead(DefaultDynamoDbExtensionContext.builder() .tableMetadata(FakeItem.getTableMetadata()) .operationContext(PRIMARY_CONTEXT) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperationTest.java index cb67cf7877b5..731dff1ae033 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperationTest.java @@ -15,11 +15,9 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; +import static org.assertj.core.api.Assertions.assertThat; import static java.util.Collections.singletonMap; import static java.util.stream.Collectors.toList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasEntry; -import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -94,6 +92,11 @@ public class QueryOperationTest { @Mock private DynamoDbEnhancedClientExtension mockDynamoDbEnhancedClientExtension; + @Test + public void returnsCorrectOperationName() { + assertThat(queryOperation.operationName().label()).isEqualTo("Query"); + } + @Test public void getServiceCall_makesTheRightCallAndReturnsResponse() { QueryRequest queryRequest = QueryRequest.builder().build(); @@ -102,7 +105,7 @@ public void getServiceCall_makesTheRightCallAndReturnsResponse() { SdkIterable response = queryOperation.serviceCall(mockDynamoDbClient).apply(queryRequest); - assertThat(response, is(mockQueryIterable)); + assertThat(response).isEqualTo(mockQueryIterable); verify(mockDynamoDbClient).queryPaginator(queryRequest); } @@ -115,7 +118,7 @@ public void getAsyncServiceCall_makesTheRightCallAndReturnsResponse() { SdkPublisher response = queryOperation.asyncServiceCall(mockDynamoDbAsyncClient).apply(queryRequest); - assertThat(response, is(mockQueryPublisher)); + assertThat(response).isEqualTo(mockQueryPublisher); verify(mockDynamoDbAsyncClient).queryPaginator(queryRequest); } @@ -135,7 +138,7 @@ public void generateRequest_nonDefault_usesQueryConditional() { .keyConditionExpression("test-expression") .expressionAttributeValues(keyItemMap) .build(); - assertThat(queryRequest, is(expectedQueryRequest)); + assertThat(queryRequest).isEqualTo(expectedQueryRequest); verify(mockQueryConditional).expression(FakeItem.getTableSchema(), TableMetadata.primaryIndexName()); } @@ -152,7 +155,7 @@ public void generateRequest_defaultQuery_usesEqualTo() { AttributeValue.builder().s(keyItem.getId()).build())) .expressionAttributeNames(singletonMap("#AMZN_MAPPED_id", "id")) .build(); - assertThat(queryRequest, is(expectedQueryRequest)); + assertThat(queryRequest).isEqualTo(expectedQueryRequest); } @Test @@ -164,7 +167,7 @@ public void generateRequest_knowsHowToUseAnIndex() { .build()); QueryRequest queryRequest = queryToTest.generateRequest(FakeItemWithIndices.getTableSchema(), GSI_1_CONTEXT, null); - assertThat(queryRequest.indexName(), is("gsi_1")); + assertThat(queryRequest.indexName()).isEqualTo("gsi_1"); } @Test @@ -178,7 +181,7 @@ public void generateRequest_ascending() { PRIMARY_CONTEXT, null); - assertThat(queryRequest.scanIndexForward(), is(true)); + assertThat(queryRequest.scanIndexForward()).isEqualTo(true); } @Test @@ -192,7 +195,7 @@ public void generateRequest_descending() { PRIMARY_CONTEXT, null); - assertThat(queryRequest.scanIndexForward(), is(false)); + assertThat(queryRequest.scanIndexForward()).isEqualTo(false); } @Test @@ -206,7 +209,7 @@ public void generateRequest_limit() { PRIMARY_CONTEXT, null); - assertThat(queryRequest.limit(), is(123)); + assertThat(queryRequest.limit()).isEqualTo(123); } @Test @@ -226,8 +229,8 @@ public void generateRequest_filterExpression_withValues() { PRIMARY_CONTEXT, null); - assertThat(queryRequest.filterExpression(), is("test-expression")); - assertThat(queryRequest.expressionAttributeValues(), hasEntry(":test-key", stringValue("test-value"))); + assertThat(queryRequest.filterExpression()).isEqualTo("test-expression"); + assertThat(queryRequest.expressionAttributeValues()).containsEntry(":test-key", stringValue("test-value")); } @Test @@ -243,7 +246,7 @@ public void generateRequest_filterExpression_withoutValues() { PRIMARY_CONTEXT, null); - assertThat(queryRequest.filterExpression(), is("test-expression")); + assertThat(queryRequest.filterExpression()).isEqualTo("test-expression"); } @Test(expected = IllegalArgumentException.class) @@ -274,7 +277,7 @@ public void generateRequest_consistentRead() { PRIMARY_CONTEXT, null); - assertThat(queryRequest.consistentRead(), is(true)); + assertThat(queryRequest.consistentRead()).isEqualTo(true); } @Test @@ -289,9 +292,9 @@ public void generateRequest_projectionExpression() { PRIMARY_CONTEXT, null); - assertThat(queryRequest.projectionExpression(), is("#AMZN_MAPPED_id,#AMZN_MAPPED_version")); - assertThat(queryRequest.expressionAttributeNames().get("#AMZN_MAPPED_id"), is ("id")); - assertThat(queryRequest.expressionAttributeNames().get("#AMZN_MAPPED_version"), is ("version")); + assertThat(queryRequest.projectionExpression()).isEqualTo("#AMZN_MAPPED_id,#AMZN_MAPPED_version"); + assertThat(queryRequest.expressionAttributeNames().get("#AMZN_MAPPED_id")).isEqualTo("id"); + assertThat(queryRequest.expressionAttributeNames().get("#AMZN_MAPPED_version")).isEqualTo("version"); } @Test @@ -310,8 +313,7 @@ public void generateRequest_hashKeyOnly_withExclusiveStartKey() { PRIMARY_CONTEXT, null); - assertThat(queryRequest.exclusiveStartKey(), - hasEntry("id", AttributeValue.builder().s(exclusiveStartKey.getId()).build())); + assertThat(queryRequest.exclusiveStartKey()).containsEntry("id", AttributeValue.builder().s(exclusiveStartKey.getId()).build()); } @Test @@ -332,14 +334,10 @@ public void generateRequest_secondaryIndex_exclusiveStartKeyUsesPrimaryAndSecond GSI_1_CONTEXT, null); - assertThat(queryRequest.exclusiveStartKey(), - hasEntry("id", AttributeValue.builder().s(exclusiveStartKey.getId()).build())); - assertThat(queryRequest.exclusiveStartKey(), - hasEntry("sort", AttributeValue.builder().s(exclusiveStartKey.getSort()).build())); - assertThat(queryRequest.exclusiveStartKey(), - hasEntry("gsi_id", AttributeValue.builder().s(exclusiveStartKey.getGsiId()).build())); - assertThat(queryRequest.exclusiveStartKey(), - hasEntry("gsi_sort", AttributeValue.builder().s(exclusiveStartKey.getGsiSort()).build())); + assertThat(queryRequest.exclusiveStartKey()).containsEntry("id", AttributeValue.builder().s(exclusiveStartKey.getId()).build()); + assertThat(queryRequest.exclusiveStartKey()).containsEntry("sort", AttributeValue.builder().s(exclusiveStartKey.getSort()).build()); + assertThat(queryRequest.exclusiveStartKey()).containsEntry("gsi_id", AttributeValue.builder().s(exclusiveStartKey.getGsiId()).build()); + assertThat(queryRequest.exclusiveStartKey()).containsEntry("gsi_sort", AttributeValue.builder().s(exclusiveStartKey.getGsiSort()).build()); } @Test @@ -361,10 +359,8 @@ public void generateRequest_hashAndSortKey_withExclusiveStartKey() { PRIMARY_CONTEXT, null); - assertThat(queryRequest.exclusiveStartKey(), - hasEntry("id", AttributeValue.builder().s(exclusiveStartKey.getId()).build())); - assertThat(queryRequest.exclusiveStartKey(), - hasEntry("sort", AttributeValue.builder().s(exclusiveStartKey.getSort()).build())); + assertThat(queryRequest.exclusiveStartKey()).containsEntry("id", AttributeValue.builder().s(exclusiveStartKey.getId()).build()); + assertThat(queryRequest.exclusiveStartKey()).containsEntry("sort", AttributeValue.builder().s(exclusiveStartKey.getSort()).build()); } @Test @@ -380,7 +376,7 @@ public void transformResults_multipleItems_returnsCorrectItems() { PRIMARY_CONTEXT, null); - assertThat(queryResultPage.items(), is(queryResultItems)); + assertThat(queryResultPage.items()).isEqualTo(queryResultItems); } @Test @@ -398,7 +394,7 @@ public void transformResults_multipleItems_setsLastEvaluatedKey() { PRIMARY_CONTEXT, null); - assertThat(queryResultPage.lastEvaluatedKey(), is(getAttributeValueMap(lastEvaluatedKey))); + assertThat(queryResultPage.lastEvaluatedKey()).isEqualTo(getAttributeValueMap(lastEvaluatedKey)); } @Test @@ -426,7 +422,7 @@ public void queryItem_withExtension_correctlyTransformsItem() { PRIMARY_CONTEXT, mockDynamoDbEnhancedClientExtension); - assertThat(queryResultPage.items(), is(modifiedResultItems)); + assertThat(queryResultPage.items()).isEqualTo(modifiedResultItems); InOrder inOrder = Mockito.inOrder(mockDynamoDbEnhancedClientExtension); queryResultMap.forEach( attributeMap -> inOrder.verify(mockDynamoDbEnhancedClientExtension) @@ -490,7 +486,7 @@ public void generateRequest_gsiCompositeKeys_partitionKeysOnly() { .expressionAttributeValues(values) .build(); - assertThat(queryRequest, is(expectedQueryRequest)); + assertThat(queryRequest).isEqualTo(expectedQueryRequest); } @Test @@ -511,9 +507,8 @@ public void generateRequest_gsiCompositeKeys_partitionAndSortKeys() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2")); - assertThat(queryRequest.indexName(), is("gsi1")); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2"); + assertThat(queryRequest.indexName()).isEqualTo("gsi1"); } @Test @@ -543,8 +538,7 @@ public void generateRequest_gsiCompositeKeys_sortBetween() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 BETWEEN :AMZN_MAPPED_gsiSort2 AND :AMZN_MAPPED_gsiSort22")); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 BETWEEN :AMZN_MAPPED_gsiSort2 AND :AMZN_MAPPED_gsiSort22"); } @Test @@ -565,8 +559,7 @@ public void generateRequest_gsiCompositeKeys_sortBeginsWith() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND begins_with(#AMZN_MAPPED_gsiSort2, :AMZN_MAPPED_gsiSort2)")); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND begins_with(#AMZN_MAPPED_gsiSort2, :AMZN_MAPPED_gsiSort2)"); } @Test @@ -596,16 +589,11 @@ public void generateRequest_gsiCompositeKeys_exclusiveStartKey() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.exclusiveStartKey(), - hasEntry("id", AttributeValue.builder().s("id1").build())); - assertThat(queryRequest.exclusiveStartKey(), - hasEntry("gsiKey1", AttributeValue.builder().s("gsiKey1").build())); - assertThat(queryRequest.exclusiveStartKey(), - hasEntry("gsiKey2", AttributeValue.builder().s("gsiKey2").build())); - assertThat(queryRequest.exclusiveStartKey(), - hasEntry("gsiSort1", AttributeValue.builder().s("gsiSort1").build())); - assertThat(queryRequest.exclusiveStartKey(), - hasEntry("gsiSort2", AttributeValue.builder().s("gsiSort2").build())); + assertThat(queryRequest.exclusiveStartKey()).containsEntry("id", AttributeValue.builder().s("id1").build()); + assertThat(queryRequest.exclusiveStartKey()).containsEntry("gsiKey1", AttributeValue.builder().s("gsiKey1").build()); + assertThat(queryRequest.exclusiveStartKey()).containsEntry("gsiKey2", AttributeValue.builder().s("gsiKey2").build()); + assertThat(queryRequest.exclusiveStartKey()).containsEntry("gsiSort1", AttributeValue.builder().s("gsiSort1").build()); + assertThat(queryRequest.exclusiveStartKey()).containsEntry("gsiSort2", AttributeValue.builder().s("gsiSort2").build()); } @Test @@ -628,7 +616,7 @@ public void transformResults_gsiCompositeKeys_multipleItems() { Page queryResultPage = queryOperation.transformResponse(queryResponse, CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryResultPage.items(), is(queryResultItems)); + assertThat(queryResultPage.items()).isEqualTo(queryResultItems); } @Test @@ -641,8 +629,8 @@ public void generateRequest_gsiCompositeKeys_backwardCompatibility() { QueryRequest queryRequest = queryToTest.generateRequest(FakeItemWithIndices.getTableSchema(), GSI_1_CONTEXT, null); - assertThat(queryRequest.indexName(), is("gsi_1")); - assertThat(queryRequest.keyConditionExpression(), is("#AMZN_MAPPED_gsi_id = :AMZN_MAPPED_gsi_id")); + assertThat(queryRequest.indexName()).isEqualTo("gsi_1"); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsi_id = :AMZN_MAPPED_gsi_id"); } @Test @@ -659,8 +647,7 @@ public void generateRequest_gsiCompositeKeys_consumerBuilder() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1")); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1"); } @Test(expected = IllegalArgumentException.class) @@ -734,9 +721,8 @@ public void generateRequest_gsiCompositeKeys_partitionValuesFromStrings() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2")); - assertThat(queryRequest.indexName(), is("gsi1")); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2"); + assertThat(queryRequest.indexName()).isEqualTo("gsi1"); } @Test @@ -755,8 +741,7 @@ public void generateRequest_gsiCompositeKeys_partitionAndSortFromStrings() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2")); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2"); } @Test @@ -773,10 +758,9 @@ public void generateRequest_gsiCompositeKeys_partitionValuesFromNumbers() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2")); - assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey1").n(), is("123")); - assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey2").n(), is("456")); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2"); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey1").n()).isEqualTo("123"); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey2").n()).isEqualTo("456"); } @Test @@ -802,8 +786,7 @@ public void generateRequest_gsiCompositeKeys_sortBetweenFromNumbers() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 BETWEEN :AMZN_MAPPED_gsiSort2 AND :AMZN_MAPPED_gsiSort22")); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 BETWEEN :AMZN_MAPPED_gsiSort2 AND :AMZN_MAPPED_gsiSort22"); } @Test @@ -823,10 +806,9 @@ public void generateRequest_gsiCompositeKeys_partitionValuesFromBinary() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2")); - assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey1").b(), is(bytes1)); - assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey2").b(), is(bytes2)); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2"); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey1").b()).isEqualTo(bytes1); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey2").b()).isEqualTo(bytes2); } @Test @@ -848,8 +830,7 @@ public void generateRequest_gsiCompositeKeys_sortBeginsWithFromBinary() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND begins_with(#AMZN_MAPPED_gsiSort1, :AMZN_MAPPED_gsiSort1)")); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND begins_with(#AMZN_MAPPED_gsiSort1, :AMZN_MAPPED_gsiSort1)"); } @Test @@ -868,12 +849,11 @@ public void generateRequest_gsiCompositeKeys_mixedTypes() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2")); - assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey1").s(), is("key1")); - assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey2").s(), is("key2")); - assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiSort1").n(), is("123")); - assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiSort2").n(), is("456")); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2"); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey1").s()).isEqualTo("key1"); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey2").s()).isEqualTo("key2"); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiSort1").n()).isEqualTo("123"); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiSort2").n()).isEqualTo("456"); } @Test @@ -888,8 +868,7 @@ public void generateRequest_gsiCompositeKeys_consumerBuilderFromStrings() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1")); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1"); } @Test @@ -904,7 +883,6 @@ public void generateRequest_gsiCompositeKeys_consumerBuilderFromNumbers() { QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); - assertThat(queryRequest.keyConditionExpression(), - is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1")); + assertThat(queryRequest.keyConditionExpression()).isEqualTo("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1"); } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/TransactGetItemsOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/TransactGetItemsOperationTest.java index 09d39a435f2f..479719a191b3 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/TransactGetItemsOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/TransactGetItemsOperationTest.java @@ -15,13 +15,10 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; +import static org.assertj.core.api.Assertions.assertThat; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.sameInstance; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -103,6 +100,12 @@ public void setupMappedTables() { fakeItemWithSortMappedTable = enhancedClient.table(TABLE_NAME_2, FakeItemWithSort.getTableSchema()); } + @Test + public void returnsCorrectOperationName() { + TransactGetItemsOperation operation = TransactGetItemsOperation.create(emptyRequest()); + assertThat(operation.operationName().label()).isEqualTo("TransactGetItems"); + } + @Test public void generateRequest_getsFromMultipleTables_usingShortcutForm() { TransactGetItemsEnhancedRequest transactGetItemsEnhancedRequest = @@ -127,7 +130,7 @@ public void generateRequest_getsFromMultipleTables_usingShortcutForm() { TransactGetItemsRequest actualRequest = operation.generateRequest(null); - assertThat(actualRequest, is(expectedRequest)); + assertThat(actualRequest).isEqualTo(expectedRequest); } @Test @@ -151,7 +154,7 @@ public void getServiceCall_makesTheRightCallAndReturnsResponse_usingKeyItemForm( TransactGetItemsResponse response = operation.serviceCall(mockDynamoDbClient).apply(transactGetItemsRequest); - assertThat(response, sameInstance(expectedResponse)); + assertThat(response).isSameAs(expectedResponse); verify(mockDynamoDbClient).transactGetItems(transactGetItemsRequest); } @@ -170,10 +173,10 @@ public void transformResponse_noExtension_returnsItemsFromDifferentTables() { List result = operation.transformResponse(response, null); - assertThat(result, contains(DefaultDocument.create(FAKE_ITEM_MAPS.get(0)), + assertThat(result).containsExactly(DefaultDocument.create(FAKE_ITEM_MAPS.get(0)), DefaultDocument.create(FAKESORT_ITEM_MAPS.get(0)), DefaultDocument.create(FAKESORT_ITEM_MAPS.get(1)), - DefaultDocument.create(FAKE_ITEM_MAPS.get(1)))); + DefaultDocument.create(FAKE_ITEM_MAPS.get(1))); } @Test @@ -208,9 +211,9 @@ public void transformResponse_noExtension_returnsNullsAsNulls() { List result = operation.transformResponse(response, null); - assertThat(result, contains(DefaultDocument.create(FAKE_ITEM_MAPS.get(0)), + assertThat(result).containsExactly(DefaultDocument.create(FAKE_ITEM_MAPS.get(0)), DefaultDocument.create(FAKESORT_ITEM_MAPS.get(0)), - null)); + null); } @Test @@ -227,9 +230,9 @@ public void transformResponse_noExtension_returnsEmptyAsNull() { List result = operation.transformResponse(response, null); - assertThat(result, contains(DefaultDocument.create(FAKE_ITEM_MAPS.get(0)), + assertThat(result).containsExactly(DefaultDocument.create(FAKE_ITEM_MAPS.get(0)), DefaultDocument.create(FAKESORT_ITEM_MAPS.get(0)), - DefaultDocument.create(emptyMap()))); + DefaultDocument.create(emptyMap())); } private static TransactGetItemsEnhancedRequest emptyRequest() { diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/TransactWriteItemsOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/TransactWriteItemsOperationTest.java index 16a0adcfa880..ce2e7dd03789 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/TransactWriteItemsOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/TransactWriteItemsOperationTest.java @@ -15,10 +15,8 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; +import static org.assertj.core.api.Assertions.assertThat; import static java.util.Collections.singletonList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.sameInstance; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -83,6 +81,17 @@ public void setupMappedTables() { fakeItemMappedTable = enhancedClient.table(TABLE_NAME, FakeItem.getTableSchema()); } + @Test + public void returnsCorrectOperationName() { + TransactWriteItemsEnhancedRequest transactGetItemsEnhancedRequest = + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(fakeItemMappedTable, fakeItem1) + .build(); + TransactWriteItemsOperation operation = TransactWriteItemsOperation.create(transactGetItemsEnhancedRequest); + + assertThat(operation.operationName().label()).isEqualTo("TransactWriteItems"); + } + @Test public void generateRequest_singleTransaction() { TransactWriteItemsEnhancedRequest transactGetItemsEnhancedRequest = @@ -96,7 +105,7 @@ public void generateRequest_singleTransaction() { .transactItems(fakeTransactWriteItem1) .build(); - assertThat(actualRequest, is(expectedRequest)); + assertThat(actualRequest).isEqualTo(expectedRequest); verifyNoMoreInteractions(mockDynamoDbEnhancedClientExtension); } @@ -115,7 +124,7 @@ public void generateRequest_multipleTransactions() { .transactItems(fakeTransactWriteItem1, fakeTransactWriteItem2) .build(); - assertThat(actualRequest, is(expectedRequest)); + assertThat(actualRequest).isEqualTo(expectedRequest); verifyNoMoreInteractions(mockDynamoDbEnhancedClientExtension); } @@ -126,7 +135,7 @@ public void generateRequest_noTransactions() { TransactWriteItemsRequest actualRequest = operation.generateRequest(mockDynamoDbEnhancedClientExtension); TransactWriteItemsRequest expectedRequest = TransactWriteItemsRequest.builder().build(); - assertThat(actualRequest, is(expectedRequest)); + assertThat(actualRequest).isEqualTo(expectedRequest); verifyNoMoreInteractions(mockDynamoDbEnhancedClientExtension); } @@ -145,7 +154,7 @@ public void getServiceCall_callsServiceAndReturnsResult() { TransactWriteItemsResponse actualResponse = operation.serviceCall(mockDynamoDbClient).apply(request); - assertThat(actualResponse, is(sameInstance(expectedResponse))); + assertThat(actualResponse).isSameAs(expectedResponse); verify(mockDynamoDbClient).transactWriteItems(request); verifyNoMoreInteractions(mockDynamoDbEnhancedClientExtension); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java index 66fabd520e46..6b154c95f6c4 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java @@ -15,16 +15,9 @@ package software.amazon.awssdk.enhanced.dynamodb.mapper; +import static org.assertj.core.api.Assertions.assertThat; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasEntry; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.not; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.binaryValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; @@ -35,18 +28,22 @@ import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.List; +import org.apache.logging.log4j.core.LogEvent; +import org.assertj.core.api.Assertions; import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; +import org.slf4j.event.Level; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.ExecutionContext; +import software.amazon.awssdk.enhanced.dynamodb.LogCaptor; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; @@ -56,6 +53,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AttributeConverterBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AttributeConverterNoConstructorBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CommonTypesBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CompositeKeyMaxBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.DocumentBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.DuplicateOrderBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.EmptyConverterProvidersInvalidBean; @@ -70,13 +68,15 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FluentSetterBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.IgnoredAttributeBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CompositeKeyMaxBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ListBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MapBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MixedCompositeBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MixedOrderingBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MultipleConverterProvidersBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NestedBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NestedBeanIgnoreNulls; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NoConstructorConverterProvidersBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NonSequentialOrderBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ParameterizedAbstractBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ParameterizedDocumentBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.PrimitiveTypesBean; @@ -87,11 +87,8 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SingleConverterProvidersBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SortKeyBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.TwoPartitionKeyBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ThreeSortKeyBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MixedCompositeBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NonSequentialOrderBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MixedOrderingBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.TwoPartitionKeyBean; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @RunWith(MockitoJUnitRunner.class) @@ -108,47 +105,47 @@ public void tearDown() { @Test public void simpleBean_correctlyAssignsPrimaryPartitionKey() { BeanTableSchema beanTableSchema = BeanTableSchema.create(SimpleBean.class); - assertThat(beanTableSchema.tableMetadata().primaryPartitionKey(), is("id")); + assertThat(beanTableSchema.tableMetadata().primaryPartitionKey()).isEqualTo("id"); } @Test public void sortKeyBean_correctlyAssignsSortKey() { BeanTableSchema beanTableSchema = BeanTableSchema.create(SortKeyBean.class); - assertThat(beanTableSchema.tableMetadata().primarySortKey(), is(Optional.of("sort"))); + assertThat(beanTableSchema.tableMetadata().primarySortKey()).isEqualTo(Optional.of("sort")); } @Test public void simpleBean_hasNoSortKey() { BeanTableSchema beanTableSchema = BeanTableSchema.create(SimpleBean.class); - assertThat(beanTableSchema.tableMetadata().primarySortKey(), is(Optional.empty())); + assertThat(beanTableSchema.tableMetadata().primarySortKey()).isEqualTo(Optional.empty()); } @Test public void simpleBean_hasNoAdditionalKeys() { BeanTableSchema beanTableSchema = BeanTableSchema.create(SimpleBean.class); - assertThat(beanTableSchema.tableMetadata().allKeys(), contains("id")); + assertThat(beanTableSchema.tableMetadata().allKeys()).containsExactly("id"); } @Test public void sortKeyBean_hasNoAdditionalKeys() { BeanTableSchema beanTableSchema = BeanTableSchema.create(SortKeyBean.class); - assertThat(beanTableSchema.tableMetadata().allKeys(), containsInAnyOrder("id", "sort")); + assertThat(beanTableSchema.tableMetadata().allKeys()).containsExactlyInAnyOrder("id", "sort"); } @Test public void secondaryIndexBean_definesGsiCorrectly() { BeanTableSchema beanTableSchema = BeanTableSchema.create(SecondaryIndexBean.class); - assertThat(beanTableSchema.tableMetadata().indexPartitionKey("gsi"), is("sort")); - assertThat(beanTableSchema.tableMetadata().indexSortKey("gsi"), is(Optional.of("attribute"))); + assertThat(beanTableSchema.tableMetadata().indexPartitionKey("gsi")).isEqualTo("sort"); + assertThat(beanTableSchema.tableMetadata().indexSortKey("gsi")).isEqualTo(Optional.of("attribute")); } @Test public void secondaryIndexBean_definesLsiCorrectly() { BeanTableSchema beanTableSchema = BeanTableSchema.create(SecondaryIndexBean.class); - assertThat(beanTableSchema.tableMetadata().indexPartitionKey("lsi"), is("id")); - assertThat(beanTableSchema.tableMetadata().indexSortKey("lsi"), is(Optional.of("attribute"))); + assertThat(beanTableSchema.tableMetadata().indexPartitionKey("lsi")).isEqualTo("id"); + assertThat(beanTableSchema.tableMetadata().indexSortKey("lsi")).isEqualTo(Optional.of("attribute")); } @Test @@ -160,8 +157,8 @@ public void dynamoDbIgnore_propertyIsIgnored() { Map itemMap = beanTableSchema.itemToMap(ignoredAttributeBean, false); - assertThat(itemMap.size(), is(1)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap.size()).isEqualTo(1); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); } @Test @@ -173,8 +170,8 @@ public void transient_propertyIsIgnored() { Map itemMap = beanTableSchema.itemToMap(ignoredAttributeBean, false); - assertThat(itemMap.size(), is(1)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap.size()).isEqualTo(1); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); } @Test @@ -185,18 +182,18 @@ public void setterAnnotations_alsoWork() { setterAnnotatedBean.setIntegerAttribute(123); setterAnnotatedBean.setInteger2Attribute(123); - assertThat(beanTableSchema.tableMetadata().primaryPartitionKey(), is("id")); + assertThat(beanTableSchema.tableMetadata().primaryPartitionKey()).isEqualTo("id"); Map itemMap = beanTableSchema.itemToMap(setterAnnotatedBean, false); - assertThat(itemMap.size(), is(1)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap.size()).isEqualTo(1); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); } @Test public void dynamoDbAttribute_remapsAttributeName() { BeanTableSchema beanTableSchema = BeanTableSchema.create(RemappedAttributeBean.class); - assertThat(beanTableSchema.tableMetadata().primaryPartitionKey(), is("remappedAttribute")); + assertThat(beanTableSchema.tableMetadata().primaryPartitionKey()).isEqualTo("remappedAttribute"); } @Test @@ -210,10 +207,10 @@ public void dynamoDbFlatten_correctlyFlattensBeanAttributes() { flattenedBeanBean.setAbstractBean(abstractBean); Map itemMap = beanTableSchema.itemToMap(flattenedBeanBean, false); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("attribute2", stringValue("two"))); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("attribute2", stringValue("two")); } @Test @@ -226,7 +223,7 @@ public void dynamoDbPreserveEmptyObject_shouldInitializeAsEmptyClass() { Map itemMap = beanTableSchema.itemToMap(bean, true); NestedBean nestedBean = beanTableSchema.mapToItem(itemMap); - assertThat(nestedBean.getInnerBean(), is(innerPreserveEmptyBean)); + assertThat(nestedBean.getInnerBean()).isEqualTo(innerPreserveEmptyBean); } @Test @@ -240,9 +237,9 @@ public void dynamoDbIgnoreNulls_shouldOmitNulls() { Map itemMap = beanTableSchema.itemToMap(bean, true); AttributeValue expectedMapForInnerBean1 = AttributeValue.builder().m(new HashMap<>()).build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("innerBean1", expectedMapForInnerBean1)); - assertThat(itemMap.get("innerBean2").m(), hasEntry("attribute2", nullAttributeValue())); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("innerBean1", expectedMapForInnerBean1); + assertThat(itemMap.get("innerBean2").m()).containsEntry("attribute2", nullAttributeValue()); } @Test @@ -259,9 +256,9 @@ public void dynamoDbIgnoreNulls_onList_shouldOmitNulls() { .l(l -> l.m(singletonMap("attribute2", nullAttributeValue()))) .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("innerBeanList1", expectedMapForInnerBean1)); - assertThat(itemMap, hasEntry("innerBeanList2", expectedMapForInnerBean2)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("innerBeanList1", expectedMapForInnerBean1); + assertThat(itemMap).containsEntry("innerBeanList2", expectedMapForInnerBean2); } @Test @@ -274,10 +271,10 @@ public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() { flattenedImmutableBean.setAbstractImmutable(abstractImmutable); Map itemMap = beanTableSchema.itemToMap(flattenedImmutableBean, false); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("attribute2", stringValue("two"))); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("attribute2", stringValue("two")); } @Test @@ -289,10 +286,10 @@ public void dynamoDbFlatten_correctlyFlattensNullImmutableAttributes() { flattenedImmutableBean.setAbstractImmutable(abstractImmutable); Map itemMap = beanTableSchema.itemToMap(flattenedImmutableBean, false); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", AttributeValue.fromNul(true))); - assertThat(itemMap, hasEntry("attribute2", AttributeValue.fromNul(true))); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", AttributeValue.fromNul(true)); + assertThat(itemMap).containsEntry("attribute2", AttributeValue.fromNul(true)); } @Test @@ -316,11 +313,11 @@ public void dynamoDbFlatten_correctlyFlattensNestedImmutableAttributes() { AttributeValue.builder().m(Collections.unmodifiableMap(nestedAttributesMap)).build(); Map itemMap = beanTableSchema.itemToMap(flattenedNestedImmutableBean, false); - assertThat(itemMap.size(), is(4)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("attribute2", stringValue("two"))); - assertThat(itemMap, hasEntry("abstractNestedImmutableOne", expectedNestedAttribute)); + assertThat(itemMap.size()).isEqualTo(4); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("attribute2", stringValue("two")); + assertThat(itemMap).containsEntry("abstractNestedImmutableOne", expectedNestedAttribute); } @Test @@ -334,11 +331,11 @@ public void dynamoDbFlatten_correctlyFlattensNullNestedImmutableAttributes() { flattenedNestedImmutableBean.setAbstractNestedImmutable(abstractNestedImmutable); Map itemMap = beanTableSchema.itemToMap(flattenedNestedImmutableBean, false); - assertThat(itemMap.size(), is(4)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("attribute2", AttributeValue.fromNul(true))); - assertThat(itemMap, hasEntry("abstractNestedImmutableOne", AttributeValue.fromNul(true))); + assertThat(itemMap.size()).isEqualTo(4); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("attribute2", AttributeValue.fromNul(true)); + assertThat(itemMap).containsEntry("abstractNestedImmutableOne", AttributeValue.fromNul(true)); } @Test @@ -349,11 +346,11 @@ public void dynamoDbFlatten_correctlyFlattensSecondNullNestedAttributes_IgnoreNu flattenedFirstNestedBean.setId("id-value"); Map itemMap = beanTableSchema.itemToMap(flattenedFirstNestedBean, false); - assertThat(itemMap.size(), is(4)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("secondId", AttributeValue.fromNul(true))); - assertThat(itemMap, hasEntry("thirdId", AttributeValue.fromNul(true))); - assertThat(itemMap, hasEntry("flattenedFourthBean", AttributeValue.fromNul(true))); + assertThat(itemMap.size()).isEqualTo(4); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("secondId", AttributeValue.fromNul(true)); + assertThat(itemMap).containsEntry("thirdId", AttributeValue.fromNul(true)); + assertThat(itemMap).containsEntry("flattenedFourthBean", AttributeValue.fromNul(true)); } @Test @@ -364,8 +361,8 @@ public void dynamoDbFlatten_correctlyFlattensSecondNullNestedAttributes_IgnoreNu flattenedFirstNestedBean.setId("id-value"); Map itemMap = beanTableSchema.itemToMap(flattenedFirstNestedBean, true); - assertThat(itemMap.size(), is(1)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap.size()).isEqualTo(1); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); } @Test @@ -379,11 +376,11 @@ public void dynamoDbFlatten_correctlyFlattensThirdNullNestedAttributes_IgnoreNul flattenedFirstNestedBean.setFlattenedSecondNestedBean(flattenedSecondNestedBean); Map itemMap = beanTableSchema.itemToMap(flattenedFirstNestedBean, false); - assertThat(itemMap.size(), is(4)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("secondId", stringValue("second-id-value"))); - assertThat(itemMap, hasEntry("thirdId", AttributeValue.fromNul(true))); - assertThat(itemMap, hasEntry("flattenedFourthBean", AttributeValue.fromNul(true))); + assertThat(itemMap.size()).isEqualTo(4); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("secondId", stringValue("second-id-value")); + assertThat(itemMap).containsEntry("thirdId", AttributeValue.fromNul(true)); + assertThat(itemMap).containsEntry("flattenedFourthBean", AttributeValue.fromNul(true)); } @Test @@ -397,9 +394,9 @@ public void dynamoDbFlatten_correctlyFlattensThirdNullNestedAttributes_IgnoreNul flattenedFirstNestedBean.setFlattenedSecondNestedBean(flattenedSecondNestedBean); Map itemMap = beanTableSchema.itemToMap(flattenedFirstNestedBean, true); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("secondId", stringValue("second-id-value"))); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("secondId", stringValue("second-id-value")); } @Test @@ -417,10 +414,10 @@ public void documentBean_correctlyMapsBeanAttributes() { .build(); Map itemMap = beanTableSchema.itemToMap(documentBean, true); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("abstractBean", expectedDocument)); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("abstractBean", expectedDocument); } @Test @@ -444,10 +441,10 @@ public void documentBean_list_correctlyMapsBeanAttributes() { AttributeValue expectedList = AttributeValue.builder().l(expectedDocument1, expectedDocument2).build(); Map itemMap = beanTableSchema.itemToMap(documentBean, true); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("abstractBeanList", expectedList)); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("abstractBeanList", expectedList); } @Test @@ -478,10 +475,10 @@ public void documentBean_map_correctlyMapsBeanAttributes() { AttributeValue expectedMap = AttributeValue.builder().m(expectedAttributeValueMap).build(); Map itemMap = beanTableSchema.itemToMap(documentBean, true); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("abstractBeanMap", expectedMap)); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("abstractBeanMap", expectedMap); } @Test @@ -498,10 +495,10 @@ public void documentBean_correctlyMapsImmutableAttributes() { .build(); Map itemMap = beanTableSchema.itemToMap(documentBean, true); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("abstractImmutable", expectedDocument)); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("abstractImmutable", expectedDocument); } @Test @@ -523,10 +520,10 @@ public void documentBean_list_correctlyMapsImmutableAttributes() { AttributeValue expectedList = AttributeValue.builder().l(expectedDocument1, expectedDocument2).build(); Map itemMap = beanTableSchema.itemToMap(documentBean, true); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("abstractImmutableList", expectedList)); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("abstractImmutableList", expectedList); } @Test @@ -555,10 +552,10 @@ public void documentBean_map_correctlyMapsImmutableAttributes() { AttributeValue expectedMap = AttributeValue.builder().m(expectedAttributeValueMap).build(); Map itemMap = beanTableSchema.itemToMap(documentBean, true); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("abstractImmutableMap", expectedMap)); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("abstractImmutableMap", expectedMap); } @Test @@ -576,10 +573,10 @@ public void parameterizedDocumentBean_correctlyMapsAttributes() { .build(); Map itemMap = beanTableSchema.itemToMap(documentBean, true); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("abstractBean", expectedDocument)); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("abstractBean", expectedDocument); } @Test @@ -603,10 +600,10 @@ public void parameterizedDocumentBean_list_correctlyMapsAttributes() { AttributeValue expectedList = AttributeValue.builder().l(expectedDocument1, expectedDocument2).build(); Map itemMap = beanTableSchema.itemToMap(documentBean, true); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("abstractBeanList", expectedList)); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("abstractBeanList", expectedList); } @Test @@ -637,10 +634,10 @@ public void parameterizedDocumentBean_map_correctlyMapsAttributes() { AttributeValue expectedMap = AttributeValue.builder().m(expectedAttributeValueMap).build(); Map itemMap = beanTableSchema.itemToMap(documentBean, true); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("abstractBeanMap", expectedMap)); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("abstractBeanMap", expectedMap); } @Test @@ -652,10 +649,10 @@ public void extendedBean_correctlyExtendsAttributes() { extendedBean.setAttribute2("two"); Map itemMap = beanTableSchema.itemToMap(extendedBean, false); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); - assertThat(itemMap, hasEntry("attribute2", stringValue("two"))); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("attribute1", stringValue("one")); + assertThat(itemMap).containsEntry("attribute2", stringValue("two")); } @Test(expected = IllegalArgumentException.class) @@ -671,8 +668,8 @@ public void itemToMap_nullAttribute_ignoreNullsTrue() { Map itemMap = beanTableSchema.itemToMap(simpleBean, true); - assertThat(itemMap.size(), is(1)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); + assertThat(itemMap.size()).isEqualTo(1); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); } @Test @@ -683,9 +680,9 @@ public void itemToMap_nullAttribute_ignoreNullsFalse() { Map itemMap = beanTableSchema.itemToMap(simpleBean, false); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("integerAttribute", nullAttributeValue())); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("integerAttribute", nullAttributeValue()); } @Test @@ -697,9 +694,9 @@ public void itemToMap_nonNullAttribute() { Map itemMap = beanTableSchema.itemToMap(simpleBean, false); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("integerAttribute", numberValue(123))); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("integerAttribute", numberValue(123)); } @Test @@ -714,7 +711,7 @@ public void mapToItem_createsItem() { SimpleBean result = beanTableSchema.mapToItem(itemMap); - assertThat(result, is(expectedBean)); + assertThat(result).isEqualTo(expectedBean); } @Test @@ -724,7 +721,7 @@ public void attributeValue_returnsValue() { simpleBean.setId("id-value"); simpleBean.setIntegerAttribute(123); - assertThat(beanTableSchema.attributeValue(simpleBean, "integerAttribute"), is(numberValue(123))); + assertThat(beanTableSchema.attributeValue(simpleBean, "integerAttribute")).isEqualTo(numberValue(123)); } @Test @@ -750,12 +747,12 @@ public void enumBean_singleEnum() { Map itemMap = beanTableSchema.itemToMap(enumBean, true); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("testEnum", stringValue("ONE"))); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("testEnum", stringValue("ONE")); EnumBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(enumBean))); + assertThat(reverse).isEqualTo(enumBean); } @Test @@ -771,12 +768,12 @@ public void enumBean_listEnum() { .l(stringValue("ONE"), stringValue("TWO")) .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("testEnumList", expectedAttributeValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("testEnumList", expectedAttributeValue); EnumBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(enumBean))); + assertThat(reverse).isEqualTo(enumBean); } @Test @@ -794,12 +791,12 @@ public void listBean_stringList() { stringValue("three")) .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("stringList", expectedAttributeValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("stringList", expectedAttributeValue); ListBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(listBean))); + assertThat(reverse).isEqualTo(listBean); } @Test @@ -817,12 +814,12 @@ public void listBean_stringListList() { .l(list1, list2) .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("stringListList", expectedAttributeValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("stringListList", expectedAttributeValue); ListBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(listBean))); + assertThat(reverse).isEqualTo(listBean); } @Test @@ -842,12 +839,12 @@ public void setBean_stringSet() { .ss("one", "two", "three") .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("stringSet", expectedAttributeValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("stringSet", expectedAttributeValue); SetBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(setBean))); + assertThat(reverse).isEqualTo(setBean); } @Test @@ -867,12 +864,12 @@ public void setBean_integerSet() { .ns("1", "2", "3") .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("integerSet", expectedAttributeValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("integerSet", expectedAttributeValue); SetBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(setBean))); + assertThat(reverse).isEqualTo(setBean); } @Test @@ -892,12 +889,12 @@ public void setBean_longSet() { .ns("1", "2", "3") .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("longSet", expectedAttributeValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("longSet", expectedAttributeValue); SetBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(setBean))); + assertThat(reverse).isEqualTo(setBean); } @Test @@ -917,12 +914,12 @@ public void setBean_shortSet() { .ns("1", "2", "3") .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("shortSet", expectedAttributeValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("shortSet", expectedAttributeValue); SetBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(setBean))); + assertThat(reverse).isEqualTo(setBean); } @Test @@ -942,12 +939,12 @@ public void setBean_byteSet() { .ns("1", "2", "3") .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("byteSet", expectedAttributeValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("byteSet", expectedAttributeValue); SetBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(setBean))); + assertThat(reverse).isEqualTo(setBean); } @Test @@ -967,12 +964,12 @@ public void setBean_doubleSet() { .ns("1.1", "2.2", "3.3") .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("doubleSet", expectedAttributeValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("doubleSet", expectedAttributeValue); SetBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(setBean))); + assertThat(reverse).isEqualTo(setBean); } @Test @@ -992,12 +989,12 @@ public void setBean_floatSet() { .ns("1.1", "2.2", "3.3") .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("floatSet", expectedAttributeValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("floatSet", expectedAttributeValue); SetBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(setBean))); + assertThat(reverse).isEqualTo(setBean); } @Test @@ -1021,12 +1018,12 @@ public void setBean_binarySet() { .bs(buffer1, buffer2, buffer3) .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("binarySet", expectedAttributeValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("binarySet", expectedAttributeValue); SetBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(setBean))); + assertThat(reverse).isEqualTo(setBean); } @Test @@ -1050,12 +1047,12 @@ public void mapBean_stringStringMap() { .m(expectedMap) .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("stringMap", expectedMapValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("stringMap", expectedMapValue); MapBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(mapBean))); + assertThat(reverse).isEqualTo(mapBean); } @Test @@ -1079,12 +1076,12 @@ public void mapBean_mapWithNullValue() { .m(expectedMap) .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("stringMap", expectedMapValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("stringMap", expectedMapValue); MapBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(mapBean))); + assertThat(reverse).isEqualTo(mapBean); } @Test @@ -1109,12 +1106,12 @@ public void mapBean_nestedStringMap() { .m(expectedMap) .build(); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("nestedStringMap", expectedMapValue)); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("nestedStringMap", expectedMapValue); MapBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(mapBean))); + assertThat(reverse).isEqualTo(mapBean); } @Test @@ -1135,19 +1132,19 @@ public void commonTypesBean() { Map itemMap = beanTableSchema.itemToMap(commonTypesBean, true); - assertThat(itemMap.size(), is(9)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("booleanAttribute", AttributeValue.builder().bool(true).build())); - assertThat(itemMap, hasEntry("integerAttribute", numberValue(123))); - assertThat(itemMap, hasEntry("longAttribute", numberValue(234))); - assertThat(itemMap, hasEntry("shortAttribute", numberValue(345))); - assertThat(itemMap, hasEntry("byteAttribute", numberValue(45))); - assertThat(itemMap, hasEntry("doubleAttribute", numberValue(56.7))); - assertThat(itemMap, hasEntry("floatAttribute", numberValue(67.8))); - assertThat(itemMap, hasEntry("binaryAttribute", binaryValue(binaryLiteral))); + assertThat(itemMap.size()).isEqualTo(9); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("booleanAttribute", AttributeValue.builder().bool(true).build()); + assertThat(itemMap).containsEntry("integerAttribute", numberValue(123)); + assertThat(itemMap).containsEntry("longAttribute", numberValue(234)); + assertThat(itemMap).containsEntry("shortAttribute", numberValue(345)); + assertThat(itemMap).containsEntry("byteAttribute", numberValue(45)); + assertThat(itemMap).containsEntry("doubleAttribute", numberValue(56.7)); + assertThat(itemMap).containsEntry("floatAttribute", numberValue(67.8)); + assertThat(itemMap).containsEntry("binaryAttribute", binaryValue(binaryLiteral)); CommonTypesBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(commonTypesBean))); + assertThat(reverse).isEqualTo(commonTypesBean); } @Test @@ -1166,18 +1163,18 @@ public void primitiveTypesBean() { Map itemMap = beanTableSchema.itemToMap(primitiveTypesBean, true); - assertThat(itemMap.size(), is(8)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("booleanAttribute", AttributeValue.builder().bool(true).build())); - assertThat(itemMap, hasEntry("integerAttribute", numberValue(123))); - assertThat(itemMap, hasEntry("longAttribute", numberValue(234))); - assertThat(itemMap, hasEntry("shortAttribute", numberValue(345))); - assertThat(itemMap, hasEntry("byteAttribute", numberValue(45))); - assertThat(itemMap, hasEntry("doubleAttribute", numberValue(56.7))); - assertThat(itemMap, hasEntry("floatAttribute", numberValue(67.8))); + assertThat(itemMap.size()).isEqualTo(8); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("booleanAttribute", AttributeValue.builder().bool(true).build()); + assertThat(itemMap).containsEntry("integerAttribute", numberValue(123)); + assertThat(itemMap).containsEntry("longAttribute", numberValue(234)); + assertThat(itemMap).containsEntry("shortAttribute", numberValue(345)); + assertThat(itemMap).containsEntry("byteAttribute", numberValue(45)); + assertThat(itemMap).containsEntry("doubleAttribute", numberValue(56.7)); + assertThat(itemMap).containsEntry("floatAttribute", numberValue(67.8)); PrimitiveTypesBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(primitiveTypesBean))); + assertThat(reverse).isEqualTo(primitiveTypesBean); } @Test @@ -1193,16 +1190,16 @@ public void itemToMap_specificAttributes() { Map itemMap = beanTableSchema.itemToMap(commonTypesBean, Arrays.asList("longAttribute", "floatAttribute")); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("longAttribute", numberValue(234))); - assertThat(itemMap, hasEntry("floatAttribute", numberValue(67.8))); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("longAttribute", numberValue(234)); + assertThat(itemMap).containsEntry("floatAttribute", numberValue(67.8)); } @Test public void itemType_returnsCorrectClass() { BeanTableSchema beanTableSchema = BeanTableSchema.create(SimpleBean.class); - assertThat(beanTableSchema.itemType(), is(equalTo(EnhancedType.of(SimpleBean.class)))); + assertThat(beanTableSchema.itemType()).isEqualTo(EnhancedType.of(SimpleBean.class)); } @Test @@ -1226,13 +1223,13 @@ public void usesCustomAttributeConverter() { Map itemMap = beanTableSchema.itemToMap(converterBean, false); - assertThat(itemMap.size(), is(3)); - assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("integerAttribute", numberValue(123))); - assertThat(itemMap, hasEntry("attributeItem", stringValue("inner-value"))); + assertThat(itemMap.size()).isEqualTo(3); + assertThat(itemMap).containsEntry("id", stringValue("id-value")); + assertThat(itemMap).containsEntry("integerAttribute", numberValue(123)); + assertThat(itemMap).containsEntry("attributeItem", stringValue("inner-value")); AttributeConverterBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse, is(equalTo(converterBean))); + assertThat(reverse).isEqualTo(converterBean); } @Test @@ -1252,13 +1249,13 @@ public void usesCustomAttributeConverterProvider() { Map itemMap = beanTableSchema.itemToMap(converterBean, false); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value-custom"))); - assertThat(itemMap, hasEntry("integerAttribute", numberValue(133))); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value-custom")); + assertThat(itemMap).containsEntry("integerAttribute", numberValue(133)); SingleConverterProvidersBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse.getId(), is(equalTo("id-value-custom"))); - assertThat(reverse.getIntegerAttribute(), is(equalTo(133))); + assertThat(reverse.getId()).isEqualTo("id-value-custom"); + assertThat(reverse.getIntegerAttribute()).isEqualTo(133); } @Test @@ -1272,13 +1269,13 @@ public void usesCustomAttributeConverterProviders() { Map itemMap = beanTableSchema.itemToMap(converterBean, false); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value-custom"))); - assertThat(itemMap, hasEntry("integerAttribute", numberValue(133))); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value-custom")); + assertThat(itemMap).containsEntry("integerAttribute", numberValue(133)); MultipleConverterProvidersBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse.getId(), is(equalTo("id-value-custom"))); - assertThat(reverse.getIntegerAttribute(), is(equalTo(133))); + assertThat(reverse.getId()).isEqualTo("id-value-custom"); + assertThat(reverse.getIntegerAttribute()).isEqualTo(133); } @Test @@ -1298,13 +1295,13 @@ public void emptyConverterProviderList_correct_whenAttributeConvertersAreSupplie Map itemMap = beanTableSchema.itemToMap(converterBean, false); - assertThat(itemMap.size(), is(2)); - assertThat(itemMap, hasEntry("id", stringValue("id-value-custom"))); - assertThat(itemMap, hasEntry("integerAttribute", numberValue(133))); + assertThat(itemMap.size()).isEqualTo(2); + assertThat(itemMap).containsEntry("id", stringValue("id-value-custom")); + assertThat(itemMap).containsEntry("integerAttribute", numberValue(133)); EmptyConverterProvidersValidBean reverse = beanTableSchema.mapToItem(itemMap); - assertThat(reverse.getId(), is(equalTo("id-value-custom"))); - assertThat(reverse.getIntegerAttribute(), is(equalTo(133))); + assertThat(reverse.getId()).isEqualTo("id-value-custom"); + assertThat(reverse.getIntegerAttribute()).isEqualTo(133); } @Test @@ -1319,7 +1316,7 @@ public void fluentSetterBean_correctlyMapsBeanAttributes() { Map itemMap = beanTableSchema.itemToMap(fluentSetterBean, false); - assertThat(beanTableSchema.mapToItem(itemMap), is(equalTo(fluentSetterBean))); + assertThat(beanTableSchema.mapToItem(itemMap)).isEqualTo(fluentSetterBean); } @Test @@ -1352,18 +1349,18 @@ public void compositeKeyMaxBean_fourPartitionAndFourSortKeys_correctOrder() { BeanTableSchema beanTableSchema = BeanTableSchema.create(CompositeKeyMaxBean.class); List partitionKeys = beanTableSchema.tableMetadata().indexPartitionKeys("gsi1"); - assertThat(partitionKeys, hasSize(4)); - assertThat(partitionKeys.get(0), is("gsiPk1")); - assertThat(partitionKeys.get(1), is("gsiPk2")); - assertThat(partitionKeys.get(2), is("gsiPk3")); - assertThat(partitionKeys.get(3), is("gsiPk4")); + assertThat(partitionKeys).hasSize(4); + assertThat(partitionKeys.get(0)).isEqualTo("gsiPk1"); + assertThat(partitionKeys.get(1)).isEqualTo("gsiPk2"); + assertThat(partitionKeys.get(2)).isEqualTo("gsiPk3"); + assertThat(partitionKeys.get(3)).isEqualTo("gsiPk4"); List sortKeys = beanTableSchema.tableMetadata().indexSortKeys("gsi1"); - assertThat(sortKeys, hasSize(4)); - assertThat(sortKeys.get(0), is("gsiSk1")); - assertThat(sortKeys.get(1), is("gsiSk2")); - assertThat(sortKeys.get(2), is("gsiSk3")); - assertThat(sortKeys.get(3), is("gsiSk4")); + assertThat(sortKeys).hasSize(4); + assertThat(sortKeys.get(0)).isEqualTo("gsiSk1"); + assertThat(sortKeys.get(1)).isEqualTo("gsiSk2"); + assertThat(sortKeys.get(2)).isEqualTo("gsiSk3"); + assertThat(sortKeys.get(3)).isEqualTo("gsiSk4"); } @Test @@ -1371,8 +1368,8 @@ public void compositeKeyBean_twoPartitionKeys_correctOrder() { BeanTableSchema beanTableSchema = BeanTableSchema.create(TwoPartitionKeyBean.class); List partitionKeys = beanTableSchema.tableMetadata().indexPartitionKeys("gsi1"); - assertThat(partitionKeys, hasSize(2)); - assertThat(partitionKeys, contains("key2", "key1")); + assertThat(partitionKeys).hasSize(2); + assertThat(partitionKeys).containsExactly("key2", "key1"); } @Test @@ -1380,8 +1377,8 @@ public void compositeKeyBean_threeSortKeys_correctOrder() { BeanTableSchema beanTableSchema = BeanTableSchema.create(ThreeSortKeyBean.class); List sortKeys = beanTableSchema.tableMetadata().indexSortKeys("gsi1"); - assertThat(sortKeys, hasSize(3)); - assertThat(sortKeys, contains("sort2", "sort3", "sort1")); + assertThat(sortKeys).hasSize(3); + assertThat(sortKeys).containsExactly("sort2", "sort3", "sort1"); } @Test @@ -1389,12 +1386,12 @@ public void compositeKeyBean_mixedComposite_twoPartitionThreeSort() { BeanTableSchema beanTableSchema = BeanTableSchema.create(MixedCompositeBean.class); List partitionKeys = beanTableSchema.tableMetadata().indexPartitionKeys("gsi1"); - assertThat(partitionKeys, hasSize(2)); - assertThat(partitionKeys, contains("pk1", "pk2")); + assertThat(partitionKeys).hasSize(2); + assertThat(partitionKeys).containsExactly("pk1", "pk2"); List sortKeys = beanTableSchema.tableMetadata().indexSortKeys("gsi1"); - assertThat(sortKeys, hasSize(3)); - assertThat(sortKeys, contains("sk2", "sk1", "sk3")); + assertThat(sortKeys).hasSize(3); + assertThat(sortKeys).containsExactly("sk2", "sk1", "sk3"); } @Test @@ -1422,10 +1419,23 @@ public void compositeKeyBean_mixedExplicitImplicitOrdering_throwsException() { public void rootSchema_areCached_but_flattenedAreNot() { BeanTableSchema root1 = BeanTableSchema.create(CompositeKeyMaxBean.class, ExecutionContext.ROOT); BeanTableSchema root2 = BeanTableSchema.create(CompositeKeyMaxBean.class, ExecutionContext.ROOT); - assertThat(root1, is(root2)); + assertThat(root1).isEqualTo(root2); BeanTableSchema flattened = BeanTableSchema.create(CompositeKeyMaxBean.class, ExecutionContext.FLATTENED); - assertThat(root1, not(flattened)); + assertThat(root1).isNotEqualTo(flattened); + } + + @Test + public void whenCreatingBeanTableSchema_logsDebugMessage() { + try (LogCaptor logCaptor = new LogCaptor("software.amazon.awssdk.enhanced.dynamodb.beans", Level.DEBUG)) { + + BeanTableSchema.create(SimpleBean.class); + + List logEvents = logCaptor.loggedEvents(); + Assertions.assertThat(logEvents.get(0).getLevel().name()).isEqualTo(Level.DEBUG.name()); + Assertions.assertThat(logEvents.get(0).getMessage().getFormattedMessage()) + .contains("software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean - Creating bean schema"); + } } @DynamoDbBean diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchemaTest.java index 24d2feef7e65..33eea634a883 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchemaTest.java @@ -15,17 +15,12 @@ package software.amazon.awssdk.enhanced.dynamodb.mapper; +import static org.assertj.core.api.Assertions.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.stream.Collectors.toList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasEntry; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; @@ -801,7 +796,7 @@ public Consumer modifyMetadata() { @Test public void itemType_returnsCorrectClass() { - assertThat(FakeItem.getTableSchema().itemType(), is(equalTo(EnhancedType.of(FakeItem.class)))); + assertThat(FakeItem.getTableSchema().itemType()).isEqualTo(EnhancedType.of(FakeItem.class)); } @Test @@ -811,25 +806,72 @@ public void itemType_returnsCorrectClassWhenBuiltWithEnhancedType() { .attributes(ATTRIBUTES) .build(); - assertThat(tableSchema.itemType(), is(equalTo(EnhancedType.of(FakeMappedItem.class)))); + assertThat(tableSchema.itemType()).isEqualTo(EnhancedType.of(FakeMappedItem.class)); + } + + @Test + public void attributes_varargs_setsAttributesCorrectly() { + StaticAttribute attr1 = StaticAttribute.builder(FakeMappedItem.class, String.class) + .name("attr1") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .build(); + + StaticAttribute attr2 = StaticAttribute.builder(FakeMappedItem.class, Boolean.class) + .name("attr2") + .getter(FakeMappedItem::getABoolean) + .setter(FakeMappedItem::setABoolean) + .build(); + + StaticTableSchema schema = StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .attributes(attr1, attr2) + .build(); + + FakeMappedItem item = FakeMappedItem.builder().aString("test").aBoolean(true).build(); + Map result = schema.itemToMap(item, false); + + assertThat(result.size()).isEqualTo(2); + assertThat(result).containsEntry("attr1", AttributeValue.builder().s("test").build()); + assertThat(result).containsEntry("attr2", AttributeValue.builder().bool(true).build()); + } + + @Test + public void attribute_setsAttributeCorrectly() { + StaticAttribute attr = StaticAttribute.builder(FakeMappedItem.class, String.class) + .name("attr") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .build(); + + StaticTableSchema schema = StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(attr) + .build(); + + FakeMappedItem item = FakeMappedItem.builder().aString("test").aBoolean(true).build(); + Map result = schema.itemToMap(item, false); + + assertThat(result.size()).isEqualTo(1); + assertThat(result).containsEntry("attr", AttributeValue.builder().s("test").build()); } @Test public void getTableMetadata_hasCorrectFields() { TableMetadata tableMetadata = FakeItemWithSort.getTableSchema().tableMetadata(); - assertThat(tableMetadata.primaryPartitionKey(), is("id")); - assertThat(tableMetadata.primarySortKey(), is(Optional.of("sort"))); + assertThat(tableMetadata.primaryPartitionKey()).isEqualTo("id"); + assertThat(tableMetadata.primarySortKey()).isEqualTo(Optional.of("sort")); } @Test public void itemToMap_returnsCorrectMapWithMultipleAttributes() { Map attributeMap = createSimpleTableSchema().itemToMap(FAKE_ITEM, false); - assertThat(attributeMap.size(), is(3)); - assertThat(attributeMap, hasEntry("a_boolean", ATTRIBUTE_VALUE_B)); - assertThat(attributeMap, hasEntry("a_primitive_boolean", ATTRIBUTE_VALUE_B)); - assertThat(attributeMap, hasEntry("a_string", ATTRIBUTE_VALUE_S)); + assertThat(attributeMap.size()).isEqualTo(3); + assertThat(attributeMap).containsEntry("a_boolean", ATTRIBUTE_VALUE_B); + assertThat(attributeMap).containsEntry("a_primitive_boolean", ATTRIBUTE_VALUE_B); + assertThat(attributeMap).containsEntry("a_string", ATTRIBUTE_VALUE_S); } @Test @@ -837,8 +879,8 @@ public void itemToMap_omitsNullAttributes() { FakeMappedItem fakeMappedItemWithNulls = FakeMappedItem.builder().aPrimitiveBoolean(true).build(); Map attributeMap = createSimpleTableSchema().itemToMap(fakeMappedItemWithNulls, true); - assertThat(attributeMap.size(), is(1)); - assertThat(attributeMap, hasEntry("a_primitive_boolean", ATTRIBUTE_VALUE_B)); + assertThat(attributeMap.size()).isEqualTo(1); + assertThat(attributeMap).containsEntry("a_primitive_boolean", ATTRIBUTE_VALUE_B); } @Test @@ -846,9 +888,9 @@ public void itemToMap_filtersAttributes() { Map attributeMap = createSimpleTableSchema() .itemToMap(FAKE_ITEM, asList("a_boolean", "a_string")); - assertThat(attributeMap.size(), is(2)); - assertThat(attributeMap, hasEntry("a_boolean", ATTRIBUTE_VALUE_B)); - assertThat(attributeMap, hasEntry("a_string", ATTRIBUTE_VALUE_S)); + assertThat(attributeMap.size()).isEqualTo(2); + assertThat(attributeMap).containsEntry("a_boolean", ATTRIBUTE_VALUE_B); + assertThat(attributeMap).containsEntry("a_string", ATTRIBUTE_VALUE_S); } @Test(expected = IllegalArgumentException.class) @@ -866,7 +908,7 @@ public void mapToItem_returnsCorrectItemWithMultipleAttributes() { FakeMappedItem fakeMappedItem = createSimpleTableSchema().mapToItem(Collections.unmodifiableMap(attributeValueMap)); - assertThat(fakeMappedItem, is(FAKE_ITEM)); + assertThat(fakeMappedItem).isEqualTo(FAKE_ITEM); } @Test @@ -1275,7 +1317,7 @@ public void getAttributeValue_correctlyMapsSuperclassAttributes() { AttributeValue attributeValue = FakeItem.getTableSchema().attributeValue(fakeItem, "subclass_attribute"); - assertThat(attributeValue, is(AttributeValue.builder().s("subclass-value").build())); + assertThat(attributeValue).isEqualTo(AttributeValue.builder().s("subclass-value").build()); } @Test @@ -1286,7 +1328,7 @@ public void getAttributeValue_correctlyMapsComposedClassAttributes() { AttributeValue attributeValue = FakeItem.getTableSchema().attributeValue(fakeItem, "composed_attribute"); - assertThat(attributeValue, is(AttributeValue.builder().s("composed-value").build())); + assertThat(attributeValue).isEqualTo(AttributeValue.builder().s("composed-value").build()); } @Test @@ -1297,13 +1339,12 @@ public void mapToItem_correctlyConstructsComposedClass() { FakeItem fakeItem = FakeItem.getTableSchema().mapToItem(itemMap); - assertThat(fakeItem, - is(FakeItem.builder() + assertThat(fakeItem).isEqualTo(FakeItem.builder() .id("id-value") .composedObject(FakeItemComposedClass.builder() .composedAttribute("composed-value") .build()) - .build())); + .build()); } @Test @@ -1315,7 +1356,7 @@ public void buildAbstractTableSchema() { .setter(FakeMappedItem::setAString)) .build(); - assertThat(tableSchema.itemToMap(FAKE_ITEM, false), is(singletonMap("aString", stringValue("test-string")))); + assertThat(tableSchema.itemToMap(FAKE_ITEM, false)).isEqualTo(singletonMap("aString", stringValue("test-string"))); exception.expect(UnsupportedOperationException.class); exception.expectMessage("abstract"); @@ -1334,8 +1375,7 @@ public void buildAbstractWithFlatten() { FakeDocument document = FakeDocument.of("test-string", null); FakeMappedItem item = FakeMappedItem.builder().aFakeDocument(document).build(); - assertThat(tableSchema.itemToMap(item, true), - is(singletonMap("documentString", AttributeValue.builder().s("test-string").build()))); + assertThat(tableSchema.itemToMap(item, true)).isEqualTo(singletonMap("documentString", AttributeValue.builder().s("test-string").build())); } @Test @@ -1355,12 +1395,11 @@ public void buildAbstractExtends() { FakeAbstractSubclass item = new FakeAbstractSubclass(); item.setAString("test-string"); - assertThat(subclassTableSchema.itemToMap(item, true), - is(singletonMap("aString", AttributeValue.builder().s("test-string").build()))); + assertThat(subclassTableSchema.itemToMap(item, true)).isEqualTo(singletonMap("aString", AttributeValue.builder().s("test-string").build())); } @Test - public void buildAbstractTagWith() { + public void buildAbstractTagsWith() { StaticTableSchema abstractTableSchema = StaticTableSchema @@ -1368,12 +1407,11 @@ public void buildAbstractTagWith() { .tags(new TestStaticTableTag()) .build(); - assertThat(abstractTableSchema.tableMetadata().customMetadataObject(TABLE_TAG_KEY, String.class), - is(Optional.of(TABLE_TAG_VALUE))); + assertThat(abstractTableSchema.tableMetadata().customMetadataObject(TABLE_TAG_KEY, String.class)).isEqualTo(Optional.of(TABLE_TAG_VALUE)); } @Test - public void buildConcreteTagWith() { + public void buildConcreteTagsWith() { StaticTableSchema concreteTableSchema = StaticTableSchema @@ -1382,8 +1420,55 @@ public void buildConcreteTagWith() { .tags(new TestStaticTableTag()) .build(); - assertThat(concreteTableSchema.tableMetadata().customMetadataObject(TABLE_TAG_KEY, String.class), - is(Optional.of(TABLE_TAG_VALUE))); + assertThat(concreteTableSchema.tableMetadata().customMetadataObject(TABLE_TAG_KEY, String.class)).isEqualTo(Optional.of(TABLE_TAG_VALUE)); + } + + @Test + public void buildAbstractTagsCollection() { + + StaticTableSchema abstractTableSchema = + StaticTableSchema + .builder(FakeDocument.class) + .tags(singletonList(new TestStaticTableTag())) + .build(); + + assertThat(abstractTableSchema.tableMetadata().customMetadataObject(TABLE_TAG_KEY, String.class)).isEqualTo(Optional.of(TABLE_TAG_VALUE)); + } + + @Test + public void buildConcreteTagsCollection() { + + StaticTableSchema concreteTableSchema = + StaticTableSchema + .builder(FakeDocument.class) + .newItemSupplier(FakeDocument::new) + .tags(singletonList(new TestStaticTableTag())) + .build(); + + assertThat(concreteTableSchema.tableMetadata().customMetadataObject(TABLE_TAG_KEY, String.class)).isEqualTo(Optional.of(TABLE_TAG_VALUE)); + } + + @Test + public void buildAbstractAddTag() { + StaticTableSchema abstractTableSchema = + StaticTableSchema + .builder(FakeDocument.class) + .addTag(new TestStaticTableTag()) + .build(); + + assertThat(abstractTableSchema.tableMetadata().customMetadataObject(TABLE_TAG_KEY, String.class)).isEqualTo(Optional.of(TABLE_TAG_VALUE)); + } + + @Test + public void buildConcreteAddTag() { + StaticTableSchema concreteTableSchema = + StaticTableSchema + .builder(FakeDocument.class) + .newItemSupplier(FakeDocument::new) + .addTag(new TestStaticTableTag()) + .build(); + + assertThat(concreteTableSchema.tableMetadata().customMetadataObject(TABLE_TAG_KEY, String.class)).isEqualTo(Optional.of(TABLE_TAG_VALUE)); } @Test @@ -1417,7 +1502,7 @@ public void addSingleAttributeConverterProvider() { .attributeConverterProviders(provider1) .build(); - assertThat(tableSchema.attributeConverterProvider(), is(provider1)); + assertThat(tableSchema.attributeConverterProvider()).isEqualTo(provider1); } @Test @@ -1439,7 +1524,7 @@ public void usesCustomAttributeConverterProvider() { Map resultMap = tableSchema.itemToMap(FakeMappedItem.builder().aString(originalString).build(), false); - assertThat(resultMap.get("aString").s(), is(expectedString)); + assertThat(resultMap.get("aString").s()).isEqualTo(expectedString); } @Test @@ -1461,7 +1546,7 @@ public void usesCustomAttributeConverterProviders() { Map resultMap = tableSchema.itemToMap(FakeMappedItem.builder().aString(originalString).build(), false); - assertThat(resultMap.get("aString").s(), is(expectedString)); + assertThat(resultMap.get("aString").s()).isEqualTo(expectedString); } @Test @@ -1497,7 +1582,7 @@ public void noConverterProvider_handlesCorrectly_whenAttributeConvertersAreSuppl Map resultMap = tableSchema.itemToMap(FakeMappedItem.builder().aString(originalString).build(), false); - assertThat(resultMap.get("aString").s(), is(expectedString)); + assertThat(resultMap.get("aString").s()).isEqualTo(expectedString); } @Test @@ -1517,8 +1602,8 @@ public void builder_canBuildForGenericClassType() { Map expectedMap = Collections.singletonMap("entity", AttributeValue.fromS("test-value")); - assertThat(envelopeTableSchema.itemToMap(testEnvelope, false), equalTo(expectedMap)); - assertThat(envelopeTableSchema.mapToItem(expectedMap).getEntity(), equalTo("test-value")); + assertThat(envelopeTableSchema.itemToMap(testEnvelope, false)).isEqualTo(expectedMap); + assertThat(envelopeTableSchema.mapToItem(expectedMap).getEntity()).isEqualTo("test-value"); } private void verifyAttribute(EnhancedType attributeType, @@ -1533,10 +1618,10 @@ private void verifyAttribute(EnhancedType attributeType, Map expectedMap = singletonMap("value", attributeValue); Map resultMap = tableSchema.itemToMap(fakeMappedItem, false); - assertThat(resultMap, is(expectedMap)); + assertThat(resultMap).isEqualTo(expectedMap); FakeMappedItem resultItem = tableSchema.mapToItem(expectedMap); - assertThat(resultItem, is(fakeMappedItem)); + assertThat(resultItem).isEqualTo(fakeMappedItem); } private void verifyNullAttribute(EnhancedType attributeType, @@ -1550,10 +1635,10 @@ private void verifyNullAttribute(EnhancedType attributeType, Map expectedMap = singletonMap("value", nullAttributeValue()); Map resultMap = tableSchema.itemToMap(fakeMappedItem, false); - assertThat(resultMap, is(expectedMap)); + assertThat(resultMap).isEqualTo(expectedMap); FakeMappedItem resultItem = tableSchema.mapToItem(expectedMap); - assertThat(resultItem, is(nullValue())); + assertThat(resultItem).isNull(); } private void verifyNullableAttribute(EnhancedType attributeType, @@ -1763,7 +1848,7 @@ public void validCompositeKeysWithExplicitOrder_succeeds() { .tags(secondarySortKey("test_gsi", Order.SECOND))) .build(); - assertThat(schema.tableMetadata().indexPartitionKeys("test_gsi"), contains("gsi_pk1", "gsi_pk2")); - assertThat(schema.tableMetadata().indexSortKeys("test_gsi"), contains("gsi_sk1", "gsi_sk2")); + assertThat(schema.tableMetadata().indexPartitionKeys("test_gsi")).containsExactly("gsi_pk1", "gsi_pk2"); + assertThat(schema.tableMetadata().indexSortKeys("test_gsi")).containsExactly("gsi_sk1", "gsi_sk2"); } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/AddActionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/AddActionTest.java index 882fe2e37bdb..be0af9b41257 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/AddActionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/AddActionTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.update; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Collections; import nl.jqno.equalsverifier.EqualsVerifier; @@ -79,4 +80,171 @@ void copy() { AddAction copy = action.toBuilder().build(); assertThat(action).isEqualTo(copy); } + + @Test + void build_withNullPath_throwsNullPointerException() { + assertThatThrownBy(() -> AddAction.builder() + .path(null) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("path"); + } + + @Test + void build_withNullValue_throwsNullPointerException() { + assertThatThrownBy(() -> AddAction.builder() + .path(PATH) + .value(null) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("value"); + } + + @Test + void build_withNullExpressionValues_throwsNullPointerException() { + assertThatThrownBy(() -> AddAction.builder() + .path(PATH) + .value(VALUE) + .expressionValues(null) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("expressionValues"); + } + + @Test + void builder_expressionNames_withNullMap_setsToNull() { + AddAction action = AddAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .expressionNames(null) + .build(); + assertThat(action.expressionNames()).isEmpty(); + } + + @Test + void builder_putExpressionName_withNullExpressionNames_createsNewMap() { + AddAction action = AddAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME) + .build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_expressionValues_withNullMap_setsToNull() { + assertThatThrownBy(() -> AddAction.builder() + .path(PATH) + .value(VALUE) + .expressionValues(null) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("expressionValues"); + } + + @Test + void builder_putExpressionValue_withNullExpressionValues_createsNewMap() { + AddAction action = AddAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build(); + assertThat(action.expressionValues()).containsEntry(VALUE_TOKEN, NUMERIC_VALUE); + } + + @Test + void builder_putExpressionValue_whenExpressionValuesIsNull_createsNewMap() { + AddAction action = AddAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build(); + assertThat(action.expressionValues()).containsEntry(VALUE_TOKEN, NUMERIC_VALUE); + } + + @Test + void builder_putExpressionName_whenExpressionNamesIsNull_createsNewMap() { + AddAction action = AddAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME) + .build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_putExpressionValue_withInitiallyNullExpressionValues_createsNewHashMap() { + AddAction.Builder builder = AddAction.builder() + .path(PATH) + .value(VALUE); + // expressionValues is initially null + builder.putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE); + AddAction action = builder.build(); + assertThat(action.expressionValues()).containsEntry(VALUE_TOKEN, NUMERIC_VALUE); + } + + @Test + void builder_putExpressionName_withInitiallyNullExpressionNames_createsNewHashMap() { + AddAction.Builder builder = AddAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE); + // expressionNames is initially null + builder.putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + AddAction action = builder.build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_putExpressionValue_whenFieldIsNull_createsNewMap() { + AddAction.Builder builder = AddAction.builder() + .path(PATH) + .value(VALUE); + // Ensure expressionValues is null initially + builder.putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE); + AddAction action = builder.build(); + assertThat(action.expressionValues()).containsEntry(VALUE_TOKEN, NUMERIC_VALUE); + } + + @Test + void builder_putExpressionName_whenFieldIsNull_createsNewMap() { + AddAction.Builder builder = AddAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE); + builder.putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + AddAction action = builder.build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_putExpressionName_whenExpressionNamesIsNotNull_addsToExistingMap() { + AddAction action = AddAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .expressionNames(Collections.singletonMap("existing", "value")) + .putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME) + .build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + assertThat(action.expressionNames()).containsEntry("existing", "value"); + } + + @Test + void builder_putExpressionValue_whenExpressionValuesIsNotNull_addsToExistingMap() { + AddAction action = AddAction.builder() + .path(PATH) + .value(VALUE) + .expressionValues(Collections.singletonMap("existing", NUMERIC_VALUE)) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build(); + assertThat(action.expressionValues()).containsEntry(VALUE_TOKEN, NUMERIC_VALUE); + assertThat(action.expressionValues()).containsEntry("existing", NUMERIC_VALUE); + } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/DeleteActionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/DeleteActionTest.java index 5c66ab345f62..60235c46f776 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/DeleteActionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/DeleteActionTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.update; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Collections; import nl.jqno.equalsverifier.EqualsVerifier; @@ -79,4 +80,147 @@ void copy() { DeleteAction copy = action.toBuilder().build(); assertThat(action).isEqualTo(copy); } + + @Test + void build_withNullPath_throwsNullPointerException() { + assertThatThrownBy(() -> DeleteAction.builder() + .path(null) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("path"); + } + + @Test + void build_withNullValue_throwsNullPointerException() { + assertThatThrownBy(() -> DeleteAction.builder() + .path(PATH) + .value(null) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("value"); + } + + @Test + void build_withNullExpressionValues_throwsNullPointerException() { + assertThatThrownBy(() -> DeleteAction.builder() + .path(PATH) + .value(VALUE) + .expressionValues(null) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("expressionValues"); + } + + @Test + void builder_expressionNames_withNullMap_setsToNull() { + DeleteAction action = DeleteAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .expressionNames(null) + .build(); + assertThat(action.expressionNames()).isEmpty(); + } + + @Test + void builder_putExpressionName_withNullExpressionNames_createsNewMap() { + DeleteAction action = DeleteAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME) + .build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_expressionValues_withNullMap_setsToNull() { + assertThatThrownBy(() -> DeleteAction.builder() + .path(PATH) + .value(VALUE) + .expressionValues(null) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("expressionValues"); + } + + @Test + void builder_putExpressionValue_withNullExpressionValues_createsNewMap() { + DeleteAction action = DeleteAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build(); + assertThat(action.expressionValues()).containsEntry(VALUE_TOKEN, NUMERIC_VALUE); + } + + @Test + void builder_putExpressionValue_whenExpressionValuesIsNull_createsNewMap() { + DeleteAction action = DeleteAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build(); + assertThat(action.expressionValues()).containsEntry(VALUE_TOKEN, NUMERIC_VALUE); + } + + @Test + void builder_putExpressionName_whenExpressionNamesIsNull_createsNewMap() { + DeleteAction action = DeleteAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME) + .build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_putExpressionValue_whenExpressionValuesIsNotNull_addsToExistingMap() { + DeleteAction action = DeleteAction.builder() + .path(PATH) + .value(VALUE) + .expressionValues(Collections.singletonMap("existing", NUMERIC_VALUE)) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build(); + assertThat(action.expressionValues()).containsEntry("existing", NUMERIC_VALUE); + assertThat(action.expressionValues()).containsEntry(VALUE_TOKEN, NUMERIC_VALUE); + } + + @Test + void builder_putExpressionName_whenExpressionNamesIsNotNull_addsToExistingMap() { + DeleteAction action = DeleteAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .expressionNames(Collections.singletonMap("existing", "existingValue")) + .putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME) + .build(); + assertThat(action.expressionNames()).containsEntry("existing", "existingValue"); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_putExpressionValue_withInitiallyNullExpressionValues_createsNewHashMap() { + DeleteAction.Builder builder = DeleteAction.builder() + .path(PATH) + .value(VALUE); + builder.putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE); + DeleteAction action = builder.build(); + assertThat(action.expressionValues()).containsEntry(VALUE_TOKEN, NUMERIC_VALUE); + } + + @Test + void builder_putExpressionName_withInitiallyNullExpressionNames_createsNewHashMap() { + DeleteAction.Builder builder = DeleteAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE); + builder.putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + DeleteAction action = builder.build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/RemoveActionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/RemoveActionTest.java index a37239459b7b..787f9c9090f3 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/RemoveActionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/RemoveActionTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.update; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Collections; import nl.jqno.equalsverifier.EqualsVerifier; @@ -62,4 +63,60 @@ void copy() { RemoveAction copy = action.toBuilder().build(); assertThat(action).isEqualTo(copy); } + + @Test + void build_withNullPath_throwsNullPointerException() { + assertThatThrownBy(() -> RemoveAction.builder() + .path(null) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("path"); + } + + @Test + void builder_expressionNames_withNullMap_setsToNull() { + RemoveAction action = RemoveAction.builder() + .path(PATH) + .expressionNames(null) + .build(); + assertThat(action.expressionNames()).isEmpty(); + } + + @Test + void builder_putExpressionName_withNullExpressionNames_createsNewMap() { + RemoveAction action = RemoveAction.builder() + .path(PATH) + .putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME) + .build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_putExpressionName_whenExpressionNamesIsNull_createsNewMap() { + RemoveAction action = RemoveAction.builder() + .path(PATH) + .putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME) + .build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_putExpressionName_whenExpressionNamesIsNotNull_addsToExistingMap() { + RemoveAction action = RemoveAction.builder() + .path(PATH) + .expressionNames(Collections.singletonMap("existing", "existingValue")) + .putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME) + .build(); + assertThat(action.expressionNames()).containsEntry("existing", "existingValue"); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_putExpressionName_withInitiallyNullExpressionNames_createsNewHashMap() { + RemoveAction.Builder builder = RemoveAction.builder() + .path(PATH); + builder.putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + RemoveAction action = builder.build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/SetActionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/SetActionTest.java index 92d46d05dc87..fc13004c460d 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/SetActionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/SetActionTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.update; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Collections; import nl.jqno.equalsverifier.EqualsVerifier; @@ -79,4 +80,149 @@ void copy() { SetAction copy = action.toBuilder().build(); assertThat(action).isEqualTo(copy); } + + @Test + void build_withNullPath_throwsNullPointerException() { + assertThatThrownBy(() -> SetAction.builder() + .path(null) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("path"); + } + + @Test + void build_withNullValue_throwsNullPointerException() { + assertThatThrownBy(() -> SetAction.builder() + .path(PATH) + .value(null) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("value"); + } + + @Test + void build_withNullExpressionValues_throwsNullPointerException() { + assertThatThrownBy(() -> SetAction.builder() + .path(PATH) + .value(VALUE) + .expressionValues(null) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("expressionValues"); + } + + @Test + void builder_expressionNames_withNullMap_setsToNull() { + SetAction action = SetAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .expressionNames(null) + .build(); + assertThat(action.expressionNames()).isEmpty(); + } + + @Test + void builder_putExpressionName_withNullExpressionNames_createsNewMap() { + SetAction action = SetAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME) + .build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_expressionValues_withNullMap_setsToNull() { + assertThatThrownBy(() -> SetAction.builder() + .path(PATH) + .value(VALUE) + .expressionValues(null) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("expressionValues"); + } + + @Test + void builder_putExpressionValue_withNullExpressionValues_createsNewMap() { + SetAction action = SetAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build(); + assertThat(action.expressionValues()).containsEntry(VALUE_TOKEN, NUMERIC_VALUE); + } + + @Test + void builder_putExpressionName_whenExpressionNamesIsNull_createsNewMap() { + SetAction action = SetAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME) + .build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_putExpressionName_whenExpressionNamesIsNotNull_addsToExistingMap() { + SetAction action = SetAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .expressionNames(Collections.singletonMap("existing", "existingValue")) + .putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME) + .build(); + assertThat(action.expressionNames()).containsEntry("existing", "existingValue"); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_putExpressionName_withInitiallyNullExpressionNames_createsNewHashMap() { + SetAction.Builder builder = SetAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE); + builder.putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + SetAction action = builder.build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_putExpressionValue_whenFieldIsNull_createsNewMap() { + SetAction.Builder builder = SetAction.builder() + .path(PATH) + .value(VALUE); + builder.putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE); + SetAction action = builder.build(); + assertThat(action.expressionValues()).containsEntry(VALUE_TOKEN, NUMERIC_VALUE); + } + + @Test + void builder_putExpressionName_whenFieldIsNull_createsNewMap() { + SetAction.Builder builder = SetAction.builder() + .path(PATH) + .value(VALUE) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE); + // Ensure expressionNames is null initially + builder.putExpressionName(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + SetAction action = builder.build(); + assertThat(action.expressionNames()).containsEntry(ATTRIBUTE_TOKEN, ATTRIBUTE_NAME); + } + + @Test + void builder_putExpressionValue_whenExpressionValuesIsNotNull_addsToExistingMap() { + SetAction action = SetAction.builder() + .path(PATH) + .value(VALUE) + .expressionValues(Collections.singletonMap("existing", NUMERIC_VALUE)) + .putExpressionValue(VALUE_TOKEN, NUMERIC_VALUE) + .build(); + assertThat(action.expressionValues()).containsEntry(VALUE_TOKEN, NUMERIC_VALUE); + assertThat(action.expressionValues()).containsEntry("existing", NUMERIC_VALUE); + } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/UpdateExpressionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/UpdateExpressionTest.java index ed9c7850a84d..bde93ee37e91 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/UpdateExpressionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/UpdateExpressionTest.java @@ -174,4 +174,34 @@ void merge_expression_with_all_action_types() { private static final class UnknownUpdateAction implements UpdateAction { } + + @Test + void mergeExpressions_withFirstExpressionNull_returnsSecondExpression() { + UpdateExpression updateExpression = UpdateExpression.builder() + .actions(removeAction, setAction, deleteAction, addAction) + .build(); + UpdateExpression result = UpdateExpression.mergeExpressions(null, updateExpression); + assertThat(result.removeActions()).containsExactly(removeAction); + assertThat(result.setActions()).containsExactly(setAction); + assertThat(result.deleteActions()).containsExactly(deleteAction); + assertThat(result.addActions()).containsExactly(addAction); + } + + @Test + void mergeExpressions_withBothExpressionsNull_returnsNull() { + UpdateExpression result = UpdateExpression.mergeExpressions(null, null); + assertThat(result).isNull(); + } + + @Test + void builder_actions_withNullList_doesNotModifyExistingActions() { + UpdateExpression updateExpression = UpdateExpression.builder() + .addAction(removeAction) + .actions((List) null) + .build(); + assertThat(updateExpression.removeActions()).containsExactly(removeAction); + assertThat(updateExpression.setActions()).isEmpty(); + assertThat(updateExpression.deleteActions()).isEmpty(); + assertThat(updateExpression.addActions()).isEmpty(); + } } \ No newline at end of file