From 8dd1add279c12ff0b486a09b526ff063d258b471 Mon Sep 17 00:00:00 2001 From: andreasgrafenberger Date: Fri, 13 Feb 2026 14:09:37 +0200 Subject: [PATCH] Increase code coverage on dynamodb-enhanced module --- ...DefaultAttributeConverterProviderTest.java | 66 ++ ...efaultMethodsUnsupportedOperationTest.java | 179 +++++ .../dynamodb/EqualsAndHashCodeTest.java | 209 ++++++ .../awssdk/enhanced/dynamodb/LogCaptor.java | 117 +++ .../TableMetadataCompositeKeyTest.java | 10 + .../enhanced/dynamodb/TableSchemaTest.java | 83 +++ .../enhanced/dynamodb/UuidTestUtils.java | 33 + .../attribute/EnumAttributeConverterTest.java | 13 +- .../document/DefaultEnhancedDocumentTest.java | 253 ++++++- .../document/DocumentTableSchemaTest.java | 92 ++- ...GeneratedTimestampRecordExtensionTest.java | 46 ++ .../AutoGeneratedUuidExtensionTest.java | 54 ++ .../VersionedRecordExtensionTest.java | 106 ++- .../AnnotatedBeanTableSchemaTest.java | 616 ++++++++++++++++ .../AnnotatedImmutableTableSchemaTest.java | 651 +++++++++++++++- .../AsyncAnnotatedBeanTableSchemaTest.java | 643 ++++++++++++++++ ...syncAnnotatedImmutableTableSchemaTest.java | 697 ++++++++++++++++++ .../functionaltests/AtomicCounterTest.java | 26 +- .../AtomicCounterExtensionTest.java | 343 +++++++++ .../AutoGeneratedUuidExtensionTest.java | 320 ++++++++ .../extensions/ChainExtensionTest.java | 156 ++++ .../VersionedRecordExtensionTest.java | 243 ++++++ .../functionaltests/models/AbstractBean.java | 67 ++ .../models/AbstractImmutable.java | 90 +++ .../functionaltests/models/SimpleBean.java | 71 ++ .../models/SimpleImmutable.java | 94 +++ .../internal/EnhancedClientUtilsTest.java | 305 +++++++- .../client/DefaultDynamoDbAsyncTableTest.java | 2 + .../conditional/BetweenConditionalTest.java | 176 +++++ .../mapper/BeanAttributeGetterTest.java | 58 ++ .../mapper/BeanAttributeSetterTest.java | 64 ++ .../mapper/ObjectConstructorTest.java | 55 ++ .../mapper/StaticIndexMetadataTest.java | 380 ++++++++++ .../operations/BatchGetItemOperationTest.java | 235 +++++- .../BatchWriteItemOperationTest.java | 36 +- .../operations/CreateTableOperationTest.java | 8 + .../operations/DeleteItemOperationTest.java | 13 + .../operations/DeleteTableOperationTest.java | 16 +- .../DescribeTableOperationTest.java | 6 + .../operations/GetItemOperationTest.java | 11 + .../operations/QueryOperationTest.java | 5 + .../TransactGetItemsOperationTest.java | 6 + .../TransactWriteItemsOperationTest.java | 11 + .../dynamodb/mapper/BeanTableSchemaTest.java | 31 +- .../mapper/StaticTableSchemaTest.java | 103 ++- .../dynamodb/update/AddActionTest.java | 168 +++++ .../dynamodb/update/DeleteActionTest.java | 144 ++++ .../dynamodb/update/RemoveActionTest.java | 57 ++ .../dynamodb/update/SetActionTest.java | 146 ++++ .../dynamodb/update/UpdateExpressionTest.java | 30 + 50 files changed, 7282 insertions(+), 62 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/DefaultAttributeConverterProviderTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/DefaultMethodsUnsupportedOperationTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EqualsAndHashCodeTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/LogCaptor.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/UuidTestUtils.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtensionTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedBeanTableSchemaTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAnnotatedBeanTableSchemaTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAnnotatedImmutableTableSchemaTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AtomicCounterExtensionTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/ChainExtensionTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/VersionedRecordExtensionTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AbstractBean.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AbstractImmutable.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/SimpleBean.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/SimpleImmutable.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/BetweenConditionalTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanAttributeGetterTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanAttributeSetterTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectConstructorTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticIndexMetadataTest.java 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..7bdb1a4bd557 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/DefaultMethodsUnsupportedOperationTest.java @@ -0,0 +1,179 @@ +/* + * 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.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() { + return scanPackageForClasses(BASE_PACKAGE) + .filter(Class::isInterface) + .filter(this::hasDefaultMethods) + .collect(toList()) + .stream() + .flatMap(this::createTestsForInterface) + .collect(toList()) + .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..32d67b081649 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EqualsAndHashCodeTest.java @@ -0,0 +1,209 @@ +/* + * 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.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/UuidTestUtils.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/UuidTestUtils.java new file mode 100644 index 000000000000..4c17679de481 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/UuidTestUtils.java @@ -0,0 +1,33 @@ +/* + * 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.UUID; + +public final class UuidTestUtils { + + private UuidTestUtils() { + } + + public static boolean isValidUuid(String uuid) { + try { + UUID.fromString(uuid); + return true; + } catch (Exception e) { + return false; + } + } +} 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..027ae8d12caa 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 { @@ -71,11 +72,17 @@ public void transformFromWithNames_returnsName() { public void transformToWithNames_returnsEnum() { EnumAttributeConverter personConverter = EnumAttributeConverter.createWithNameAsKeys(Person.class); - Person john = personConverter.transformTo(AttributeValue.fromS("JOHN")); + personConverter.transformTo(AttributeValue.fromS("JOHN")); + } - assertThat(Person.JOHN.toString()).isEqualTo("I am a cool person"); + @Test + public void transformTo_whenInputStringIsNull_throwsIllegalArgumentException() { + EnumAttributeConverter vehicleConverter = EnumAttributeConverter.create(Vehicle.class); + AttributeValue input = AttributeValue.builder().build(); - assertThat(john).isEqualTo(Person.JOHN); + 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..2f86469405d3 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtensionTest.java @@ -0,0 +1,46 @@ +/* + * 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 rebuilt = extension.toBuilder(); + + assertThat(rebuilt).isNotNull(); + } + + @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..7bdb7e4cf3fb 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 @@ -65,6 +65,34 @@ public class AutoGeneratedUuidExtensionTest { .setter(ItemWithUuid::setSimpleString)) .build(); + @Test + public void beforeWrite_withNullCustomMetadataObject_returnsNoWriteModifications() { + StaticTableSchema schemaWithoutUuidAttribute = + 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 = schemaWithoutUuidAttribute.itemToMap(item, true); + + WriteModification result = atomicCounterExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(schemaWithoutUuidAttribute.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT).build()); + + assertThat(result).isNotNull(); + assertThat(result.transformedItem()).isNull(); + assertThat(result.updateExpression()).isNull(); + assertThat(result.additionalConditionalExpression()).isNull(); + } + @Test public void beforeWrite_updateItemOperation_hasUuidInItem_doesNotCreateUpdateExpressionAndFilters() { ItemWithUuid SimpleItem = new ItemWithUuid(); @@ -156,6 +184,32 @@ void IllegalArgumentException_for_AutogeneratedUuid_withNonStringType() { + " to be used as a Auto Generated Uuid attribute. Only String Class type is supported."); } + @Test + public void beforeWrite_noCustomMetadata_returnsEmptyModification() { + StaticTableSchema schemaWithoutMetadata = + 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 = schemaWithoutMetadata.itemToMap(item, true); + + WriteModification result = atomicCounterExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(schemaWithoutMetadata.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + + assertThat(result).isEqualTo(WriteModification.builder().build()); + } + public static boolean isValidUuid(String uuid) { return UUID_PATTERN.matcher(uuid).matches(); } 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..81622bd9b495 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 @@ -19,8 +19,11 @@ 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; @@ -41,6 +44,7 @@ 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.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; @@ -273,7 +277,22 @@ 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)); + } + + @Test + public void builder_startAtValueIsLessThanMinusOne_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, + () -> VersionedRecordExtension.builder().startAt(-2L).build(), + "startAt must be -1 or greater"); + } + + @Test + public void builder_incrementByValueIsLessThanOne_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, + () -> VersionedRecordExtension.builder().incrementBy(0L).build(), + "incrementBy must be greater than 0."); } @Test @@ -681,4 +700,89 @@ public static Stream customIncrementForExistingVersionValues() { Arguments.of(3L, null, 10L, "11"), Arguments.of(null, 3L, 4L, "7")); } + + @Test + public void versionAttribute_withInvalidStartAt_throwsIllegalArgumentException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> + StaticTableSchema.builder(TestItem.class) + .newItemSupplier(TestItem::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(TestItem::getId) + .setter(TestItem::setId) + .addTag(primaryPartitionKey())) + .addAttribute(Long.class, + a -> a.name("version") + .getter(TestItem::getVersion) + .setter(TestItem::setVersion) + .addTag(versionAttribute(-2L, 1L))) + .build() + ) + .withMessage("startAt must be -1 or greater."); + } + + @Test + public void versionAttribute_withInvalidIncrementBy_throwsIllegalArgumentException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> + StaticTableSchema.builder(TestItem.class) + .newItemSupplier(TestItem::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(TestItem::getId) + .setter(TestItem::setId) + .addTag(primaryPartitionKey())) + .addAttribute(Long.class, + a -> a.name("version") + .getter(TestItem::getVersion) + .setter(TestItem::setVersion) + .addTag(versionAttribute(0L, 0L))) + .build() + ) + .withMessage("incrementBy must be greater than 0."); + } + + @Test + public void versionAttribute_withNonNumericType_throwsIllegalArgumentException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> + StaticTableSchema.builder(TestItem.class) + .newItemSupplier(TestItem::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(TestItem::getId) + .setter(TestItem::setId) + .addTag(primaryPartitionKey())) + .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 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/AnnotatedBeanTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedBeanTableSchemaTest.java new file mode 100644 index 000000000000..fd6f72afc567 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AnnotatedBeanTableSchemaTest.java @@ -0,0 +1,616 @@ +/* + * 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.assertThatThrownBy; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; + +import org.assertj.core.api.Assertions; +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.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AbstractBean; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.SimpleBean; +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; + +public class AnnotatedBeanTableSchemaTest extends LocalDynamoDbSyncTestBase { + + private static final String TABLE_NAME = "table-name"; + + private static final TableSchema TABLE_SCHEMA = TableSchema.fromClass(SimpleBean.class); + + private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .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() { + 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 mappedTable = enhancedClient.table(getConcreteTableName(tableName), TABLE_SCHEMA); + mappedTable.createTable(); + + TableDescription tableDescription = mappedTable.describeTable().table(); + + String actualTableName = tableDescription.tableName(); + Long actualReadCapacityUnits = tableDescription.provisionedThroughput().readCapacityUnits(); + Long actualWriteCapacityUnits = tableDescription.provisionedThroughput().writeCapacityUnits(); + + assertThat(actualTableName, is(getConcreteTableName(tableName))); + assertThat(actualReadCapacityUnits, is(0L)); + assertThat(actualWriteCapacityUnits, is(0L)); + + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(tableName)) + .build()); + + assertThatThrownBy(mappedTable::describeTable) + .isInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("Cannot do operations on a non-existent table"); + } + + @Test + public void createTableWithProvisionedThroughput_succeeds() { + String tableName = TABLE_NAME + "-1"; + + DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName(tableName), TABLE_SCHEMA); + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + TableDescription tableDescription = mappedTable.describeTable().table(); + + String actualTableName = tableDescription.tableName(); + Long actualReadCapacityUnits = tableDescription.provisionedThroughput().readCapacityUnits(); + Long actualWriteCapacityUnits = tableDescription.provisionedThroughput().writeCapacityUnits(); + + assertThat(actualTableName, is(getConcreteTableName(tableName))); + assertThat(actualReadCapacityUnits, is(getDefaultProvisionedThroughput().readCapacityUnits())); + assertThat(actualWriteCapacityUnits, is(getDefaultProvisionedThroughput().writeCapacityUnits())); + + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(tableName)) + .build()); + + assertThatThrownBy(mappedTable::describeTable) + .isInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("Cannot do operations on a non-existent table"); + } + + @Test + public void createTableWithDefaults_throwsIllegalArgumentException() { + TableSchema tableSchema = TableSchema.fromClass(AbstractBean.class); + DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName(TABLE_NAME), tableSchema); + + assertThatThrownBy(mappedTable::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 tableSchema = TableSchema.fromClass(AbstractBean.class); + DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName(TABLE_NAME), tableSchema); + + assertThatThrownBy(() -> mappedTable.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() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + SimpleBean result = mappedTable.getItem(item); + assertThat(result, is(nullValue())); + } + + @Test + public void getItemWithResponse_itemNotFound_returnsNullValue() { + GetItemEnhancedResponse getItemEnhancedResponse = + mappedTable.getItemWithResponse(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + + assertThat(getItemEnhancedResponse.attributes(), is(nullValue())); + assertThat(getItemEnhancedResponse.consumedCapacity(), is(nullValue())); + } + + @Test + public void putItem_thenGetItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + mappedTable.putItem(item); + + SimpleBean result = mappedTable.getItem(item); + assertThat(result , is(item)); + } + + @Test + public void putItemPartial_thenGetItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + mappedTable.putItem(item); + + SimpleBean result = mappedTable.getItem(item); + assertThat(result , is(item)); + } + + @Test + public void putItemTwice_thenGetItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value-item1"); + mappedTable.putItem(item); + + item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value-item2"); + mappedTable.putItem(item); + + long itemCount = mappedTable.scan().items().stream().count(); + assertThat(itemCount, is(1L)); + + SimpleBean result = mappedTable.getItem(item); + assertThat(result, is(item)); + } + + @Test + public void putItemWithResponse_thenGetItemWithResponse_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + 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(), is(nullValue())); + assertThat(getItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void putItem_withCondition_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + mappedTable.putItem(item); + + item.setStringAttribute("stringAttribute-value-updated"); + + 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(SimpleBean.class) + .item(item) + .conditionExpression(conditionExpression) + .build()); + + SimpleBean result = mappedTable.getItem(item); + assertThat(result, is(item)); + } + + @Test + public void putItem_withCondition_throwsConditionalCheckFailedException() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + mappedTable.putItem(item); + + item.setStringAttribute("stringAttribute-value-updated"); + + 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(SimpleBean.class) + .item(item) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> mappedTable.putItem(putItemEnhancedRequest)) + .isInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void updateItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + mappedTable.putItem(item); + + item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value-updated"); + + SimpleBean result = mappedTable.updateItem(item); + assertThat(result, is(item)); + + long itemCount = mappedTable.scan().stream().count(); + assertThat(itemCount, is(1L)); + } + + @Test + public void updateItem_createsNewCompleteItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + SimpleBean result = mappedTable.updateItem(item); + assertThat(result, is(item)); + } + + @Test + public void updateItem_createsNewPartialItemThenUpdateItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + + SimpleBean result = mappedTable.updateItem(item); + assertThat(result, is(item)); + + item.setStringAttribute("stringAttribute-value"); + + result = mappedTable.updateItem(item); + assertThat(result, is(item)); + } + + @Test + public void putItem_thenUpdateItemWithNulls_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + mappedTable.updateItem(item); + + item.setStringAttribute(null); + + SimpleBean result = mappedTable.updateItem(item); + assertThat(result, is(item)); + } + + @Test + public void putItem_thenUpdateItemWithIgnoreNulls_succeeds() { + SimpleBean item1 = new SimpleBean(); + item1.setId("id-value"); + item1.setSort("sort-value"); + item1.setStringAttribute("stringAttribute-value"); + + mappedTable.putItem(item1); + + SimpleBean item2 = new SimpleBean(); + item2.setId("id-value"); + item2.setSort("sort-value"); + + UpdateItemEnhancedRequest updateItemEnhancedRequest = UpdateItemEnhancedRequest.builder(SimpleBean.class) + .item(item2) + .ignoreNulls(true) + .build(); + + SimpleBean result = mappedTable.updateItem(updateItemEnhancedRequest); + assertThat(result, is(item1)); + } + + @Test + public void putItem_thenUpdateItemWithCondition_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + mappedTable.putItem(item); + item.setStringAttribute("stringAttribute-value-updated"); + + 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(SimpleBean.class) + .item(item) + .conditionExpression(conditionExpression) + .build(); + + mappedTable.updateItem(updateItemEnhancedRequest); + + SimpleBean result = mappedTable.getItem(item); + assertThat(result, is(item)); + } + + @Test + public void putItem_thenUpdateItemWithCondition_throwsConditionalCheckFailedException() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + mappedTable.putItem(item); + + item.setStringAttribute("stringAttribute-value-updated"); + + 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(SimpleBean.class) + .item(item) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> mappedTable.updateItem(updateItemEnhancedRequest)) + .isInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndDefaultReturnValue_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + PutItemEnhancedResponse putItemEnhancedResponse = mappedTable.putItemWithResponse(r -> r.item(item)); + + item.setStringAttribute("stringAttribute-value-updated"); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = mappedTable.updateItemWithResponse(r -> r.item(item)); + + assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + assertThat(updateItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndReturnValueAllOld_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)); + + SimpleBean item2 = new SimpleBean(); + item2.setId("id-value"); + item2.setSort("sort-value"); + item2.setStringAttribute("stringAttribute-value-updated"); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + mappedTable.updateItemWithResponse(r -> r.item(item).returnValues(ReturnValue.ALL_OLD)); + + assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + assertThat(updateItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndReturnValueNone_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)); + + SimpleBean item2 = new SimpleBean(); + item2.setId("id-value"); + item2.setSort("sort-value"); + item2.setStringAttribute("stringAttribute-value-updated"); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + mappedTable.updateItemWithResponse(r -> r.item(item2).returnValues(ReturnValue.NONE)); + + assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + assertThat(updateItemEnhancedResponse.attributes(), is(nullValue())); + } + + @Test + public void deleteItem_itemNotFound_returnsNullValue() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + SimpleBean result = mappedTable.deleteItem(item); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + mappedTable.putItem(item); + + SimpleBean beforeDeleteResult = mappedTable.deleteItem(item); + SimpleBean afterDeleteResult = mappedTable.getItem(item); + + assertThat(beforeDeleteResult, is(item)); + assertThat(afterDeleteResult, is(nullValue())); + } + + @Test + public void deleteItem_withCondition_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + mappedTable.putItem(item); + + SimpleBean result = mappedTable.getItem(item); + assertThat(result, is(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, is(nullValue())); + } + + @Test + public void deleteItem_withCondition_throwsConditionalCheckFailedException() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + mappedTable.putItem(item); + + SimpleBean result = mappedTable.getItem(item); + assertThat(result, is(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() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)); + + + + Key key = mappedTable.keyFrom(item); + + DeleteItemEnhancedResponse deleteItemEnhancedResponse = + mappedTable.deleteItemWithResponse(r -> r.key(key)); + + assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + assertThat(deleteItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void deleteItemWithResponse_itemNotFound_returnsNullValue() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + Key key = mappedTable.keyFrom(item); + + DeleteItemEnhancedResponse deleteItemEnhancedResponse = + mappedTable.deleteItemWithResponse(r -> r.key(key)); + + assertThat(deleteItemEnhancedResponse.attributes(), is(nullValue())); + } +} 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 index 998f13998280..84e3d10716ea 100644 --- 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 @@ -15,50 +15,661 @@ 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 org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import org.assertj.core.api.Assertions; +import org.hamcrest.MatcherAssert; 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.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.ImmutableFakeItem; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AbstractImmutable; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.SimpleImmutable; +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.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; +import software.amazon.awssdk.services.dynamodb.model.TableDescription; public class AnnotatedImmutableTableSchemaTest extends LocalDynamoDbSyncTestBase { + private static final String TABLE_NAME = "table-name"; + private static final TableSchema TABLE_SCHEMA = TableSchema.fromClass(SimpleImmutable.class); + private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) .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() { + 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 mappedTable = enhancedClient.table(getConcreteTableName(tableName), TABLE_SCHEMA); + mappedTable.createTable(); + + TableDescription tableDescription = mappedTable.describeTable().table(); + + String actualTableName = tableDescription.tableName(); + Long actualReadCapacityUnits = tableDescription.provisionedThroughput().readCapacityUnits(); + Long actualWriteCapacityUnits = tableDescription.provisionedThroughput().writeCapacityUnits(); + + MatcherAssert.assertThat(actualTableName, is(getConcreteTableName(tableName))); + MatcherAssert.assertThat(actualReadCapacityUnits, is(0L)); + MatcherAssert.assertThat(actualWriteCapacityUnits, is(0L)); + + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(tableName)) + .build()); + + assertThatThrownBy(mappedTable::describeTable) + .isInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("Cannot do operations on a non-existent table"); + } + + @Test + public void createTableWithProvisionedThroughput_succeeds() { + String tableName = TABLE_NAME + "-1"; + + DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName(tableName), TABLE_SCHEMA); + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + TableDescription tableDescription = mappedTable.describeTable().table(); + + String actualTableName = tableDescription.tableName(); + Long actualReadCapacityUnits = tableDescription.provisionedThroughput().readCapacityUnits(); + Long actualWriteCapacityUnits = tableDescription.provisionedThroughput().writeCapacityUnits(); + + MatcherAssert.assertThat(actualTableName, is(getConcreteTableName(tableName))); + MatcherAssert.assertThat(actualReadCapacityUnits, is(getDefaultProvisionedThroughput().readCapacityUnits())); + MatcherAssert.assertThat(actualWriteCapacityUnits, is(getDefaultProvisionedThroughput().writeCapacityUnits())); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() - .tableName(getConcreteTableName(TABLE_NAME)) + .tableName(getConcreteTableName(tableName)) .build()); + + assertThatThrownBy(mappedTable::describeTable) + .isInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("Cannot do operations on a non-existent table"); + } + + @Test + public void createTableWithDefaults_throwsIllegalArgumentException() { + TableSchema tableSchema = TableSchema.fromClass(AbstractImmutable.class); + DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName(TABLE_NAME), tableSchema); + + assertThatThrownBy(mappedTable::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 tableSchema = TableSchema.fromClass(AbstractImmutable.class); + DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName(TABLE_NAME), tableSchema); + + assertThatThrownBy(() -> mappedTable.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() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + SimpleImmutable result = mappedTable.getItem(item); + MatcherAssert.assertThat(result, is(nullValue())); + } + + @Test + public void getItemWithResponse_itemNotFound_returnsNullValue() { + GetItemEnhancedResponse getItemEnhancedResponse = + mappedTable.getItemWithResponse(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + + MatcherAssert.assertThat(getItemEnhancedResponse.attributes(), is(nullValue())); + MatcherAssert.assertThat(getItemEnhancedResponse.consumedCapacity(), is(nullValue())); + } + + @Test + public void putItem_thenGetItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + mappedTable.putItem(item); + + SimpleImmutable result = mappedTable.getItem(item); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItemPartial_thenGetItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .build(); + mappedTable.putItem(item); + + SimpleImmutable result = mappedTable.getItem(item); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItemTwice_thenGetItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-item1") + .build(); + mappedTable.putItem(item); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-item2") + .build(); + mappedTable.putItem(item); + + long itemCount = mappedTable.scan().items().stream().count(); + MatcherAssert.assertThat(itemCount, is(1L)); + + SimpleImmutable result = mappedTable.getItem(item); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItemWithResponse_thenGetItemWithResponse_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)); + GetItemEnhancedResponse getItemEnhancedResponse = + mappedTable.getItemWithResponse(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + + MatcherAssert.assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + MatcherAssert.assertThat(getItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void putItem_withCondition_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + mappedTable.putItem(item); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + 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(SimpleImmutable.class) + .item(item) + .conditionExpression(conditionExpression) + .build()); + + SimpleImmutable result = mappedTable.getItem(item); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItem_withCondition_throwsConditionalCheckFailedException() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + mappedTable.putItem(item); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + 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(SimpleImmutable.class) + .item(item) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> mappedTable.putItem(putItemEnhancedRequest)) + .isInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void updateItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + mappedTable.putItem(item); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + SimpleImmutable result = mappedTable.updateItem(item); + MatcherAssert.assertThat(result, is(item)); + + long itemCount = mappedTable.scan().stream().count(); + MatcherAssert.assertThat(itemCount, is(1L)); + } + + @Test + public void updateItem_createsNewCompleteItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + SimpleImmutable result = mappedTable.updateItem(item); + MatcherAssert.assertThat(result, is(item)); } @Test - public void simpleItem_putAndGet() { - TableSchema tableSchema = - TableSchema.fromClass(ImmutableFakeItem.class); + public void updateItem_createsNewPartialItemThenUpdateItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .build(); - DynamoDbTable mappedTable = - enhancedClient.table(getConcreteTableName(TABLE_NAME), tableSchema); + SimpleImmutable result = mappedTable.updateItem(item); + MatcherAssert.assertThat(result, is(item)); - mappedTable.createTable(r -> r.provisionedThroughput(ProvisionedThroughput.builder() - .readCapacityUnits(5L) - .writeCapacityUnits(5L) - .build())); - ImmutableFakeItem immutableFakeItem = ImmutableFakeItem.builder() - .id("id123") - .attribute("test-value") - .build(); + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + result = mappedTable.updateItem(item); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItem_thenUpdateItemWithNulls_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + mappedTable.updateItem(item); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute(null) + .build(); + + SimpleImmutable result = mappedTable.updateItem(item); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItem_thenUpdateItemWithIgnoreNulls_succeeds() { + SimpleImmutable item1 = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + mappedTable.putItem(item1); + + SimpleImmutable item2 = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .build(); + + UpdateItemEnhancedRequest updateItemEnhancedRequest = + UpdateItemEnhancedRequest.builder(SimpleImmutable.class) + .item(item2) + .ignoreNulls(true) + .build(); + + SimpleImmutable result = mappedTable.updateItem(updateItemEnhancedRequest); + MatcherAssert.assertThat(result, is(item1)); + } + + @Test + public void putItem_thenUpdateItemWithCondition_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + mappedTable.putItem(item); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + 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(SimpleImmutable.class) + .item(item) + .conditionExpression(conditionExpression) + .build(); + + mappedTable.updateItem(updateItemEnhancedRequest); + + SimpleImmutable result = mappedTable.getItem(item); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItem_thenUpdateItemWithCondition_throwsConditionalCheckFailedException() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + mappedTable.putItem(item); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + 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(SimpleImmutable.class) + .item(item) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> mappedTable.updateItem(updateItemEnhancedRequest)) + .isInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndDefaultReturnValue_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + PutItemEnhancedResponse putItemEnhancedResponse = mappedTable.putItemWithResponse(r -> r.item(item)); + + SimpleImmutable item2 = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + mappedTable.updateItemWithResponse(r -> r.item(item2)); + + MatcherAssert.assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + MatcherAssert.assertThat(updateItemEnhancedResponse.attributes(), is(item2)); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndReturnValueAllOld_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)); + + + SimpleImmutable item2 = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + mappedTable.updateItemWithResponse(r -> r.item(item).returnValues(ReturnValue.ALL_OLD)); + + MatcherAssert.assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + MatcherAssert.assertThat(updateItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndReturnValueNone_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)); + + SimpleImmutable item2 = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + mappedTable.updateItemWithResponse(r -> r.item(item2).returnValues(ReturnValue.NONE)); + + MatcherAssert.assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + MatcherAssert.assertThat(updateItemEnhancedResponse.attributes(), is(nullValue())); + } + + @Test + public void deleteItem_itemNotFound_returnsNullValue() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + SimpleImmutable result = mappedTable.deleteItem(item); + MatcherAssert.assertThat(result, is(nullValue())); + } + + @Test + public void deleteItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + mappedTable.putItem(item); + + SimpleImmutable beforeDeleteResult = mappedTable.deleteItem(item); + SimpleImmutable afterDeleteResult = mappedTable.getItem(item); + + MatcherAssert.assertThat(beforeDeleteResult, is(item)); + MatcherAssert.assertThat(afterDeleteResult, is(nullValue())); + } + + @Test + public void deleteItem_withCondition_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + mappedTable.putItem(item); + + SimpleImmutable result = mappedTable.getItem(item); + MatcherAssert.assertThat(result, is(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); + MatcherAssert.assertThat(result, is(nullValue())); + } + + @Test + public void deleteItem_withCondition_throwsConditionalCheckFailedException() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + mappedTable.putItem(item); + + SimpleImmutable result = mappedTable.getItem(item); + MatcherAssert.assertThat(result, is(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() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + PutItemEnhancedResponse putItemEnhancedResponse = + mappedTable.putItemWithResponse(r -> r.item(item)); + + + Key key = mappedTable.keyFrom(item); + DeleteItemEnhancedResponse deleteItemEnhancedResponse = + mappedTable.deleteItemWithResponse(r -> r.key(key)); + + MatcherAssert.assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + MatcherAssert.assertThat(deleteItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void deleteItemWithResponse_itemNotFound_returnsNullValue() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + Key key = mappedTable.keyFrom(item); + DeleteItemEnhancedResponse deleteItemEnhancedResponse = + mappedTable.deleteItemWithResponse(r -> r.key(key)); - mappedTable.putItem(immutableFakeItem); - ImmutableFakeItem readItem = mappedTable.getItem(immutableFakeItem); - assertThat(readItem).isEqualTo(immutableFakeItem); + MatcherAssert.assertThat(deleteItemEnhancedResponse.attributes(), is(nullValue())); } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAnnotatedBeanTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAnnotatedBeanTableSchemaTest.java new file mode 100644 index 000000000000..679736125bd0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAnnotatedBeanTableSchemaTest.java @@ -0,0 +1,643 @@ +/* + * 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.assertThatThrownBy; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; + +import java.util.concurrent.CompletionException; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +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.functionaltests.models.AbstractBean; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.SimpleBean; +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; + +public class AsyncAnnotatedBeanTableSchemaTest extends LocalDynamoDbAsyncTestBase { + + private static final String TABLE_NAME = "table-name"; + + private static final TableSchema TABLE_SCHEMA = TableSchema.fromClass(SimpleBean.class); + + private final DynamoDbEnhancedAsyncClient enhancedAsyncClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .build(); + + private final DynamoDbAsyncTable asyncMappedTable = enhancedAsyncClient.table(getConcreteTableName(TABLE_NAME), + TABLE_SCHEMA); + + @Before + public void createTable() { + asyncMappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableExists(b -> b.tableName(getConcreteTableName(TABLE_NAME))).join(); + } + + @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() { + DescribeTableEnhancedResponse describeTableEnhancedResponse = asyncMappedTable.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 asyncMappedTable = enhancedAsyncClient.table(getConcreteTableName(tableName), + TABLE_SCHEMA); + asyncMappedTable.createTable().join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableExists(b -> b.tableName(getConcreteTableName(tableName))).join(); + + DescribeTableEnhancedResponse describeTableEnhancedResponse = asyncMappedTable.describeTable().join(); + TableDescription tableDescription = describeTableEnhancedResponse.table(); + + String actualTableName = tableDescription.tableName(); + Long actualReadCapacityUnits = tableDescription.provisionedThroughput().readCapacityUnits(); + Long actualWriteCapacityUnits = tableDescription.provisionedThroughput().writeCapacityUnits(); + + assertThat(actualTableName, is(getConcreteTableName(tableName))); + assertThat(actualReadCapacityUnits, is(0L)); + assertThat(actualWriteCapacityUnits, is(0L)); + + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(tableName)) + .build()).join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableNotExists(b -> b.tableName(getConcreteTableName(tableName))).join(); + + assertThatThrownBy(() -> asyncMappedTable.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 asyncMappedTable = enhancedAsyncClient.table(getConcreteTableName(tableName), + TABLE_SCHEMA); + asyncMappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableExists(b -> b.tableName(getConcreteTableName(tableName))).join(); + + DescribeTableEnhancedResponse describeTableEnhancedResponse = asyncMappedTable.describeTable().join(); + TableDescription tableDescription = describeTableEnhancedResponse.table(); + + String actualTableName = tableDescription.tableName(); + Long actualReadCapacityUnits = tableDescription.provisionedThroughput().readCapacityUnits(); + Long actualWriteCapacityUnits = tableDescription.provisionedThroughput().writeCapacityUnits(); + + assertThat(actualTableName, is(getConcreteTableName(tableName))); + assertThat(actualReadCapacityUnits, is(getDefaultProvisionedThroughput().readCapacityUnits())); + assertThat(actualWriteCapacityUnits, is(getDefaultProvisionedThroughput().writeCapacityUnits())); + + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(tableName)) + .build()).join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableNotExists(b -> b.tableName(getConcreteTableName(tableName))).join(); + + assertThatThrownBy(() -> asyncMappedTable.describeTable().join()) + .isInstanceOf(CompletionException.class) + .hasRootCauseInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("Cannot do operations on a non-existent table"); + } + + @Test + public void createTableWithDefaults_throwsIllegalArgumentException() { + TableSchema tableSchema = TableSchema.fromClass(AbstractBean.class); + DynamoDbAsyncTable asyncMappedTable = enhancedAsyncClient.table(getConcreteTableName(TABLE_NAME), + tableSchema); + + assertThatThrownBy(() -> asyncMappedTable.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 tableSchema = TableSchema.fromClass(AbstractBean.class); + DynamoDbAsyncTable asyncMappedTable = enhancedAsyncClient.table(getConcreteTableName(TABLE_NAME), + tableSchema); + + assertThatThrownBy(() -> asyncMappedTable.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() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + SimpleBean result = asyncMappedTable.getItem(item).join(); + assertThat(result, is(nullValue())); + } + + @Test + public void getItemWithResponse_itemNotFound_returnsNullValue() { + GetItemEnhancedResponse getItemEnhancedResponse = + asyncMappedTable.getItemWithResponse(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + + assertThat(getItemEnhancedResponse.attributes(), is(nullValue())); + assertThat(getItemEnhancedResponse.consumedCapacity(), is(nullValue())); + } + + @Test + public void putItem_thenGetItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + asyncMappedTable.putItem(item).join(); + + SimpleBean result = asyncMappedTable.getItem(item).join(); + assertThat(result, is(item)); + } + + @Test + public void putItemPartial_thenGetItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + asyncMappedTable.putItem(item).join(); + + SimpleBean result = asyncMappedTable.getItem(item).join(); + assertThat(result, is(item)); + } + + @Test + public void putItemTwice_thenGetItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value-item1"); + asyncMappedTable.putItem(item).join(); + + item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value-item2"); + asyncMappedTable.putItem(item).join(); + + SdkPublisher publisher = asyncMappedTable.scan().items(); + drainPublisher(publisher, 1); + + SimpleBean result = asyncMappedTable.getItem(item).join(); + assertThat(result, is(item)); + } + + @Test + public void putItemWithResponse_thenGetItemWithResponse_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + PutItemEnhancedResponse putItemEnhancedResponse = + asyncMappedTable.putItemWithResponse(r -> r.item(item)).join(); + GetItemEnhancedResponse getItemEnhancedResponse = + asyncMappedTable.getItemWithResponse(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + + assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + assertThat(getItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void putItem_withCondition_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + asyncMappedTable.putItem(item).join(); + + item.setStringAttribute("stringAttribute-value-updated"); + + 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(); + + asyncMappedTable.putItem(PutItemEnhancedRequest.builder(SimpleBean.class) + .item(item) + .conditionExpression(conditionExpression) + .build()).join(); + + SimpleBean result = asyncMappedTable.getItem(item).join(); + assertThat(result, is(item)); + } + + @Test + public void putItem_withCondition_throwsConditionalCheckFailedException() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + asyncMappedTable.putItem(item).join(); + + item.setStringAttribute("stringAttribute-value-updated"); + + 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(SimpleBean.class) + .item(item) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> asyncMappedTable.putItem(putItemEnhancedRequest).join()) + .isInstanceOf(CompletionException.class) + .hasRootCauseInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void updateItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + asyncMappedTable.putItem(item).join(); + + item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value-updated"); + + SimpleBean result = asyncMappedTable.updateItem(item).join(); + assertThat(result, is(item)); + + SdkPublisher publisher = asyncMappedTable.scan().items(); + drainPublisher(publisher, 1); + } + + @Test + public void updateItem_createsNewCompleteItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + SimpleBean result = asyncMappedTable.updateItem(item).join(); + assertThat(result, is(item)); + } + + @Test + public void updateItem_createsNewPartialItemThenUpdateItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + + SimpleBean result = asyncMappedTable.updateItem(item).join(); + assertThat(result, is(item)); + + item.setStringAttribute("stringAttribute-value"); + + result = asyncMappedTable.updateItem(item).join(); + assertThat(result, is(item)); + } + + @Test + public void putItem_thenUpdateItemWithNulls_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + asyncMappedTable.updateItem(item).join(); + + item.setStringAttribute(null); + + SimpleBean result = asyncMappedTable.updateItem(item).join(); + assertThat(result, is(item)); + } + + @Test + public void putItem_thenUpdateItemWithIgnoreNulls_succeeds() { + SimpleBean item1 = new SimpleBean(); + item1.setId("id-value"); + item1.setSort("sort-value"); + item1.setStringAttribute("stringAttribute-value"); + + asyncMappedTable.putItem(item1).join(); + + SimpleBean item2 = new SimpleBean(); + item2.setId("id-value"); + item2.setSort("sort-value"); + + UpdateItemEnhancedRequest updateItemEnhancedRequest = UpdateItemEnhancedRequest.builder(SimpleBean.class) + .item(item2) + .ignoreNulls(true) + .build(); + + SimpleBean result = asyncMappedTable.updateItem(updateItemEnhancedRequest).join(); + assertThat(result, is(item1)); + } + + @Test + public void putItem_thenUpdateItemWithCondition_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + asyncMappedTable.putItem(item).join(); + item.setStringAttribute("stringAttribute-value-updated"); + + 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(SimpleBean.class) + .item(item) + .conditionExpression(conditionExpression) + .build(); + + asyncMappedTable.updateItem(updateItemEnhancedRequest).join(); + + SimpleBean result = asyncMappedTable.getItem(item).join(); + assertThat(result, is(item)); + } + + @Test + public void putItem_thenUpdateItemWithCondition_throwsConditionalCheckFailedException() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + asyncMappedTable.putItem(item).join(); + + item.setStringAttribute("stringAttribute-value-updated"); + + 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(SimpleBean.class) + .item(item) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> asyncMappedTable.updateItem(updateItemEnhancedRequest).join()) + .isInstanceOf(CompletionException.class) + .hasRootCauseInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndDefaultReturnValue_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + PutItemEnhancedResponse putItemEnhancedResponse = + asyncMappedTable.putItemWithResponse(r -> r.item(item)).join(); + + item.setStringAttribute("stringAttribute-value-updated"); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + asyncMappedTable.updateItemWithResponse(r -> r.item(item)).join(); + + assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + assertThat(updateItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndReturnValueAllOld_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + PutItemEnhancedResponse putItemEnhancedResponse = + asyncMappedTable.putItemWithResponse(r -> r.item(item)).join(); + + SimpleBean item2 = new SimpleBean(); + item2.setId("id-value"); + item2.setSort("sort-value"); + item2.setStringAttribute("stringAttribute-value-updated"); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + asyncMappedTable.updateItemWithResponse(r -> r.item(item).returnValues(ReturnValue.ALL_OLD)).join(); + + assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + assertThat(updateItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndReturnValueNone_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + PutItemEnhancedResponse putItemEnhancedResponse = + asyncMappedTable.putItemWithResponse(r -> r.item(item)).join(); + + SimpleBean item2 = new SimpleBean(); + item2.setId("id-value"); + item2.setSort("sort-value"); + item2.setStringAttribute("stringAttribute-value-updated"); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + asyncMappedTable.updateItemWithResponse(r -> r.item(item2).returnValues(ReturnValue.NONE)).join(); + + assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + assertThat(updateItemEnhancedResponse.attributes(), is(nullValue())); + } + + @Test + public void deleteItem_itemNotFound_returnsNullValue() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + SimpleBean result = asyncMappedTable.deleteItem(item).join(); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteItem_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + asyncMappedTable.putItem(item).join(); + + SimpleBean beforeDeleteResult = asyncMappedTable.deleteItem(item).join(); + SimpleBean afterDeleteResult = asyncMappedTable.getItem(item).join(); + + assertThat(beforeDeleteResult, is(item)); + assertThat(afterDeleteResult, is(nullValue())); + } + + @Test + public void deleteItem_withCondition_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + asyncMappedTable.putItem(item).join(); + + SimpleBean result = asyncMappedTable.getItem(item).join(); + assertThat(result, is(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 = asyncMappedTable.keyFrom(item); + DeleteItemEnhancedRequest deleteItemEnhancedRequest = DeleteItemEnhancedRequest.builder() + .key(key) + .conditionExpression(conditionExpression) + .build(); + + asyncMappedTable.deleteItem(deleteItemEnhancedRequest).join(); + + result = asyncMappedTable.getItem(item).join(); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteItem_withCondition_throwsConditionalCheckFailedException() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + asyncMappedTable.putItem(item).join(); + + SimpleBean result = asyncMappedTable.getItem(item).join(); + assertThat(result, is(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 = asyncMappedTable.keyFrom(item); + + DeleteItemEnhancedRequest deleteItemEnhancedRequest = DeleteItemEnhancedRequest.builder() + .key(key) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> asyncMappedTable.deleteItem(deleteItemEnhancedRequest).join()) + .isInstanceOf(CompletionException.class) + .hasRootCauseInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void deleteItemWithResponse_succeeds() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + + PutItemEnhancedResponse putItemEnhancedResponse = + asyncMappedTable.putItemWithResponse(r -> r.item(item)).join(); + + + Key key = asyncMappedTable.keyFrom(item); + + DeleteItemEnhancedResponse deleteItemEnhancedResponse = + asyncMappedTable.deleteItemWithResponse(r -> r.key(key)).join(); + + assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + assertThat(deleteItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void deleteItemWithResponse_itemNotFound_returnsNullValue() { + SimpleBean item = new SimpleBean(); + item.setId("id-value"); + item.setSort("sort-value"); + item.setStringAttribute("stringAttribute-value"); + Key key = asyncMappedTable.keyFrom(item); + + DeleteItemEnhancedResponse deleteItemEnhancedResponse = + asyncMappedTable.deleteItemWithResponse(r -> r.key(key)).join(); + + assertThat(deleteItemEnhancedResponse.attributes(), is(nullValue())); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAnnotatedImmutableTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAnnotatedImmutableTableSchemaTest.java new file mode 100644 index 000000000000..53619d68e8a6 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAnnotatedImmutableTableSchemaTest.java @@ -0,0 +1,697 @@ +/* + * 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.assertThatThrownBy; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; + +import java.util.concurrent.CompletionException; +import org.assertj.core.api.Assertions; +import org.hamcrest.MatcherAssert; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +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.functionaltests.models.AbstractImmutable; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.SimpleImmutable; +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; + +public class AsyncAnnotatedImmutableTableSchemaTest extends LocalDynamoDbAsyncTestBase { + + private static final String TABLE_NAME = "table-name"; + + private static final TableSchema TABLE_SCHEMA = TableSchema.fromClass(SimpleImmutable.class); + + private final DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .build(); + + private final DynamoDbAsyncTable asyncMappedTable = enhancedClient.table(getConcreteTableName(TABLE_NAME), + TABLE_SCHEMA); + + @Before + public void createTable() { + asyncMappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableExists(b -> b.tableName(getConcreteTableName(TABLE_NAME))).join(); + } + + @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 = asyncMappedTable.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 asyncMappedTable = enhancedClient.table(getConcreteTableName(tableName), + TABLE_SCHEMA); + asyncMappedTable.createTable().join(); + + DescribeTableEnhancedResponse describeTableEnhancedResponse = asyncMappedTable.describeTable().join(); + TableDescription tableDescription = describeTableEnhancedResponse.table(); + + String actualTableName = tableDescription.tableName(); + Long actualReadCapacityUnits = tableDescription.provisionedThroughput().readCapacityUnits(); + Long actualWriteCapacityUnits = tableDescription.provisionedThroughput().writeCapacityUnits(); + + MatcherAssert.assertThat(actualTableName, is(getConcreteTableName(tableName))); + MatcherAssert.assertThat(actualReadCapacityUnits, is(0L)); + MatcherAssert.assertThat(actualWriteCapacityUnits, is(0L)); + + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(tableName)) + .build()).join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableNotExists(b -> b.tableName(getConcreteTableName(tableName))).join(); + + assertThatThrownBy(() -> asyncMappedTable.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 asyncMappedTable = enhancedClient.table(getConcreteTableName(tableName), + TABLE_SCHEMA); + asyncMappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + + DescribeTableEnhancedResponse describeTableEnhancedResponse = asyncMappedTable.describeTable().join(); + TableDescription tableDescription = describeTableEnhancedResponse.table(); + + String actualTableName = tableDescription.tableName(); + Long actualReadCapacityUnits = tableDescription.provisionedThroughput().readCapacityUnits(); + Long actualWriteCapacityUnits = tableDescription.provisionedThroughput().writeCapacityUnits(); + + MatcherAssert.assertThat(actualTableName, is(getConcreteTableName(tableName))); + MatcherAssert.assertThat(actualReadCapacityUnits, is(getDefaultProvisionedThroughput().readCapacityUnits())); + MatcherAssert.assertThat(actualWriteCapacityUnits, is(getDefaultProvisionedThroughput().writeCapacityUnits())); + + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName(tableName)) + .build()).join(); + + getDynamoDbAsyncClient().waiter().waitUntilTableNotExists(b -> b.tableName(getConcreteTableName(tableName))).join(); + + assertThatThrownBy(() -> asyncMappedTable.describeTable().join()) + .isInstanceOf(CompletionException.class) + .hasRootCauseInstanceOf(ResourceNotFoundException.class) + .hasMessageContaining("Cannot do operations on a non-existent table"); + } + + @Test + public void createTableWithDefaults_throwsIllegalArgumentException() { + TableSchema tableSchema = TableSchema.fromClass(AbstractImmutable.class); + DynamoDbAsyncTable asyncMappedTable = enhancedClient.table(getConcreteTableName(TABLE_NAME), + tableSchema); + + assertThatThrownBy(() -> asyncMappedTable.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 tableSchema = TableSchema.fromClass(AbstractImmutable.class); + DynamoDbAsyncTable asyncMappedTable = enhancedClient.table(getConcreteTableName(TABLE_NAME), + tableSchema); + + assertThatThrownBy(() -> asyncMappedTable.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() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + SimpleImmutable result = asyncMappedTable.getItem(item).join(); + MatcherAssert.assertThat(result, is(nullValue())); + } + + @Test + public void getItemWithResponse_itemNotFound_returnsNullValue() { + GetItemEnhancedResponse getItemEnhancedResponse = + asyncMappedTable.getItemWithResponse(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + + MatcherAssert.assertThat(getItemEnhancedResponse.attributes(), is(nullValue())); + MatcherAssert.assertThat(getItemEnhancedResponse.consumedCapacity(), is(nullValue())); + } + + @Test + public void putItem_thenGetItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + asyncMappedTable.putItem(item).join(); + + SimpleImmutable result = asyncMappedTable.getItem(item).join(); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItemPartial_thenGetItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .build(); + asyncMappedTable.putItem(item).join(); + + SimpleImmutable result = asyncMappedTable.getItem(item).join(); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItemTwice_thenGetItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-item1") + .build(); + asyncMappedTable.putItem(item).join(); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-item2") + .build(); + asyncMappedTable.putItem(item).join(); + + SdkPublisher publisher = asyncMappedTable.scan().items(); + drainPublisher(publisher, 1); + + SimpleImmutable result = asyncMappedTable.getItem(item).join(); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItemWithResponse_thenGetItemWithResponse_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + PutItemEnhancedResponse putItemEnhancedResponse = + asyncMappedTable.putItemWithResponse(r -> r.item(item)).join(); + GetItemEnhancedResponse getItemEnhancedResponse = + asyncMappedTable.getItemWithResponse(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + + MatcherAssert.assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + MatcherAssert.assertThat(getItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void putItem_withCondition_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + asyncMappedTable.putItem(item).join(); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + 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(); + + asyncMappedTable.putItem(PutItemEnhancedRequest.builder(SimpleImmutable.class) + .item(item) + .conditionExpression(conditionExpression) + .build()).join(); + + SimpleImmutable result = asyncMappedTable.getItem(item).join(); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItem_withCondition_throwsConditionalCheckFailedException() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + asyncMappedTable.putItem(item).join(); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + 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(SimpleImmutable.class) + .item(item) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> asyncMappedTable.putItem(putItemEnhancedRequest).join()) + .isInstanceOf(CompletionException.class) + .hasRootCauseInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void updateItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + asyncMappedTable.putItem(item).join(); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + SimpleImmutable result = asyncMappedTable.updateItem(item).join(); + MatcherAssert.assertThat(result, is(item)); + + SdkPublisher publisher = asyncMappedTable.scan().items(); + drainPublisher(publisher, 1); + } + + @Test + public void updateItem_createsNewCompleteItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + SimpleImmutable result = asyncMappedTable.updateItem(item).join(); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void updateItem_createsNewPartialItemThenUpdateItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .build(); + + SimpleImmutable result = asyncMappedTable.updateItem(item).join(); + MatcherAssert.assertThat(result, is(item)); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + result = asyncMappedTable.updateItem(item).join(); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItem_thenUpdateItemWithNulls_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + asyncMappedTable.updateItem(item).join(); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute(null) + .build(); + + SimpleImmutable result = asyncMappedTable.updateItem(item).join(); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItem_thenUpdateItemWithIgnoreNulls_succeeds() { + SimpleImmutable item1 = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + asyncMappedTable.putItem(item1).join(); + + SimpleImmutable item2 = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .build(); + + UpdateItemEnhancedRequest updateItemEnhancedRequest = + UpdateItemEnhancedRequest.builder(SimpleImmutable.class) + .item(item2) + .ignoreNulls(true) + .build(); + + SimpleImmutable result = asyncMappedTable.updateItem(updateItemEnhancedRequest).join(); + MatcherAssert.assertThat(result, is(item1)); + } + + @Test + public void putItem_thenUpdateItemWithCondition_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + asyncMappedTable.putItem(item).join(); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + 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(SimpleImmutable.class) + .item(item) + .conditionExpression(conditionExpression) + .build(); + + asyncMappedTable.updateItem(updateItemEnhancedRequest).join(); + + SimpleImmutable result = asyncMappedTable.getItem(item).join(); + MatcherAssert.assertThat(result, is(item)); + } + + @Test + public void putItem_thenUpdateItemWithCondition_throwsConditionalCheckFailedException() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + asyncMappedTable.putItem(item).join(); + + item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + 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(SimpleImmutable.class) + .item(item) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> asyncMappedTable.updateItem(updateItemEnhancedRequest).join()) + .isInstanceOf(CompletionException.class) + .hasRootCauseInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndDefaultReturnValue_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + PutItemEnhancedResponse putItemEnhancedResponse = + asyncMappedTable.putItemWithResponse(r -> r.item(item)).join(); + + SimpleImmutable item2 = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + asyncMappedTable.updateItemWithResponse(r -> r.item(item2)).join(); + + MatcherAssert.assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + MatcherAssert.assertThat(updateItemEnhancedResponse.attributes(), is(item2)); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndReturnValueAllOld_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + PutItemEnhancedResponse putItemEnhancedResponse = + asyncMappedTable.putItemWithResponse(r -> r.item(item)).join(); + + + SimpleImmutable item2 = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + asyncMappedTable.updateItemWithResponse(r -> r.item(item2).returnValues(ReturnValue.ALL_OLD)).join(); + + MatcherAssert.assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + MatcherAssert.assertThat(updateItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void putItemWithResponse_thenUpdateItemWithResponseAndReturnValueNone_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + PutItemEnhancedResponse putItemEnhancedResponse = + asyncMappedTable.putItemWithResponse(r -> r.item(item)).join(); + + SimpleImmutable item2 = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value-updated") + .build(); + + UpdateItemEnhancedResponse updateItemEnhancedResponse = + asyncMappedTable.updateItemWithResponse(r -> r.item(item2).returnValues(ReturnValue.NONE)).join(); + + MatcherAssert.assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + MatcherAssert.assertThat(updateItemEnhancedResponse.attributes(), is(nullValue())); + } + + @Test + public void deleteItem_itemNotFound_returnsNullValue() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + SimpleImmutable result = asyncMappedTable.deleteItem(item).join(); + MatcherAssert.assertThat(result, is(nullValue())); + } + + @Test + public void deleteItem_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + asyncMappedTable.putItem(item).join(); + + SimpleImmutable beforeDeleteResult = asyncMappedTable.deleteItem(item).join(); + SimpleImmutable afterDeleteResult = asyncMappedTable.getItem(item).join(); + + MatcherAssert.assertThat(beforeDeleteResult, is(item)); + MatcherAssert.assertThat(afterDeleteResult, is(nullValue())); + } + + @Test + public void deleteItem_withCondition_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + asyncMappedTable.putItem(item).join(); + + SimpleImmutable result = asyncMappedTable.getItem(item).join(); + MatcherAssert.assertThat(result, is(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 = asyncMappedTable.keyFrom(item); + DeleteItemEnhancedRequest deleteItemEnhancedRequest = DeleteItemEnhancedRequest.builder() + .key(key) + .conditionExpression(conditionExpression) + .build(); + + asyncMappedTable.deleteItem(deleteItemEnhancedRequest).join(); + + result = asyncMappedTable.getItem(item).join(); + MatcherAssert.assertThat(result, is(nullValue())); + } + + @Test + public void deleteItem_withCondition_throwsConditionalCheckFailedException() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + asyncMappedTable.putItem(item).join(); + + SimpleImmutable result = asyncMappedTable.getItem(item).join(); + MatcherAssert.assertThat(result, is(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 = asyncMappedTable.keyFrom(item); + DeleteItemEnhancedRequest deleteItemEnhancedRequest = DeleteItemEnhancedRequest.builder() + .key(key) + .conditionExpression(conditionExpression) + .build(); + + assertThatThrownBy(() -> asyncMappedTable.deleteItem(deleteItemEnhancedRequest).join()) + .isInstanceOf(CompletionException.class) + .hasRootCauseInstanceOf(ConditionalCheckFailedException.class) + .hasMessageContaining("The conditional request failed"); + } + + @Test + public void deleteItemWithResponse_succeeds() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + + PutItemEnhancedResponse putItemEnhancedResponse = + asyncMappedTable.putItemWithResponse(r -> r.item(item)).join(); + + + Key key = asyncMappedTable.keyFrom(item); + DeleteItemEnhancedResponse deleteItemEnhancedResponse = + asyncMappedTable.deleteItemWithResponse(r -> r.key(key)).join(); + + MatcherAssert.assertThat(putItemEnhancedResponse.attributes(), is(nullValue())); + MatcherAssert.assertThat(deleteItemEnhancedResponse.attributes(), is(item)); + } + + @Test + public void deleteItemWithResponse_itemNotFound_returnsNullValue() { + SimpleImmutable item = SimpleImmutable.builder() + .id("id-value") + .sort("sort-value") + .stringAttribute("stringAttribute-value") + .build(); + Key key = asyncMappedTable.keyFrom(item); + DeleteItemEnhancedResponse deleteItemEnhancedResponse = + asyncMappedTable.deleteItemWithResponse(r -> r.key(key)).join(); + + MatcherAssert.assertThat(deleteItemEnhancedResponse.attributes(), is(nullValue())); + } +} 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..3d90d3f2c7e6 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AtomicCounterExtensionTest.java @@ -0,0 +1,343 @@ +/* + * 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 (this == o) { + return true; + } + 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, 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 (this == o) { + return true; + } + 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, defaultCounter, customCounter); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java new file mode 100644 index 000000000000..03588232bd2b --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java @@ -0,0 +1,320 @@ +/* + * 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.UuidTestUtils.isValidUuid; + +import java.util.List; +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.AutoGeneratedUuidExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; +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.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; +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 AutoGeneratedUuidExtensionTest extends LocalDynamoDbSyncTestBase { + + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(RecordWithAutogeneratedUuid.class); + + private final DynamoDbEnhancedClient enhancedClient = + DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(Stream.concat( + ExtensionResolver.defaultExtensions().stream(), + Stream.of(AutoGeneratedUuidExtension.create())) + .collect(Collectors.toList())) + .build(); + + private final DynamoDbTable mappedTable = + enhancedClient.table(getConcreteTableName("autogenerated-uuid-table"), TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(r -> r.tableName(getConcreteTableName("autogenerated-uuid-table"))); + } + + @Test + public void putItem_whenKeysNotPopulated_generatesNewUuids() { + RecordWithAutogeneratedUuid record = new RecordWithAutogeneratedUuid(); + record.setId("existing-id"); + + mappedTable.putItem(record); + RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + } + + @Test + public void putItem_whenKeysAlreadyPopulated_replacesExistingUuids() { + RecordWithAutogeneratedUuid record = new RecordWithAutogeneratedUuid(); + record.setId("existing-id"); + record.setSortKey("existing-sk"); + + mappedTable.putItem(record); + RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + assertThat(result.getId()).isNotEqualTo("existing-id"); + assertThat(result.getSortKey()).isNotEqualTo("existing-sk"); + } + + @Test + public void batchWrite_whenKeysNotPopulated_generatesNewUuids() { + RecordWithAutogeneratedUuid record1 = new RecordWithAutogeneratedUuid(); + RecordWithAutogeneratedUuid record2 = new RecordWithAutogeneratedUuid(); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(RecordWithAutogeneratedUuid.class) + .mappedTableResource(mappedTable) + .addPutItem(record1) + .addPutItem(record2) + .build())); + List results = mappedTable.scan().items().stream().collect(Collectors.toList()); + + assertThat(results.size()).isEqualTo(2); + isValidUuid(results.get(0).getId()); + isValidUuid(results.get(1).getId()); + isValidUuid(results.get(0).getSortKey()); + isValidUuid(results.get(1).getSortKey()); + } + + @Test + public void batchWrite_whenKeysAlreadyPopulated_generatesNewUuids() { + RecordWithAutogeneratedUuid record1 = new RecordWithAutogeneratedUuid(); + record1.setId("existing-id-1"); + record1.setSortKey("existing-sk-1"); + + RecordWithAutogeneratedUuid record2 = new RecordWithAutogeneratedUuid(); + record2.setId("existing-id-2"); + record2.setSortKey("existing-sk-2"); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(RecordWithAutogeneratedUuid.class) + .mappedTableResource(mappedTable) + .addPutItem(record1) + .addPutItem(record2) + .build())); + + List results = mappedTable.scan().items().stream().collect(Collectors.toList()); + + assertThat(results.size()).isEqualTo(2); + isValidUuid(results.get(0).getId()); + isValidUuid(results.get(1).getId()); + isValidUuid(results.get(0).getSortKey()); + isValidUuid(results.get(1).getSortKey()); + + assertThat(results.get(0).getId()).isNotEqualTo("existing-id-1"); + assertThat(results.get(1).getId()).isNotEqualTo("existing-id-2"); + assertThat(results.get(0).getSortKey()).isNotEqualTo("existing-sk-1"); + assertThat(results.get(1).getSortKey()).isNotEqualTo("existing-sk-2"); + } + + @Test + public void transactWrite_whenKeysNotPopulated_generatesNewUuids() { + RecordWithAutogeneratedUuid record = new RecordWithAutogeneratedUuid(); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + } + + @Test + public void transactWrite_whenKeysAlreadyPopulated_generatesNewUuids() { + RecordWithAutogeneratedUuid record = new RecordWithAutogeneratedUuid(); + record.setId("existing-id"); + record.setSortKey("existing-sk"); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + assertThat(result.getId()).isNotEqualTo("existing-id"); + assertThat(result.getSortKey()).isNotEqualTo("existing-sk"); + } + + @Test + public void putItem_whenNoAutogeneratedUuidAnnotationIsPresent_doesNotRegenerateUuids() { + String tableName = "no-autogenerated-uuid-table"; + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(RecordWithoutAutogeneratedUuid.class)); + + try { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + RecordWithoutAutogeneratedUuid record = new RecordWithoutAutogeneratedUuid(); + record.setId("existing-id"); + record.setSortKey("existing-sk"); + record.setGsiPk("existing-gsiPk"); + record.setGsiSk("existing-gsiSk"); + record.setData("data"); + + mappedTable.putItem(record); + RecordWithoutAutogeneratedUuid retrieved = mappedTable.getItem( + r -> r.key(k -> k.partitionValue("existing-id").sortValue("existing-sk"))); + assertThat(retrieved.getId()).isEqualTo("existing-id"); + assertThat(retrieved.getSortKey()).isEqualTo("existing-sk"); + assertThat(retrieved.getGsiPk()).isEqualTo("existing-gsiPk"); + assertThat(retrieved.getGsiSk()).isEqualTo("existing-gsiSk"); + assertThat(retrieved.getData()).isEqualTo("data"); + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void createBean_givenAutogeneratedUuidAnnotationAppliedOnNonStringAttributeType_throwsException() { + assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedUuidInvalidTypeRecord.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not a suitable Java Class type to be used as a Auto Generated Uuid attribute") + .hasMessageContaining("Only String Class type is supported"); + } + + @DynamoDbBean + public static class RecordWithAutogeneratedUuid { + private String id; + private String sortKey; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + @DynamoDbAutoGeneratedUuid + public String getSortKey() { + return sortKey; + } + + public void setSortKey(String sortKey) { + this.sortKey = sortKey; + } + } + + @DynamoDbBean + public static class RecordWithoutAutogeneratedUuid { + private String id; + private String sortKey; + private String gsiPk; + private String gsiSk; + private String data; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + public String getSortKey() { + return sortKey; + } + + public void setSortKey(String sortKey) { + this.sortKey = sortKey; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1") + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + public String getGsiPk() { + return gsiPk; + } + + public void setGsiPk(String gsiPk) { + this.gsiPk = gsiPk; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1") + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getGsiSk() { + return gsiSk; + } + + public void setGsiSk(String gsiSk) { + this.gsiSk = gsiSk; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } + + @DynamoDbBean + public static class AutogeneratedUuidInvalidTypeRecord { + private Integer id; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + } +} \ No newline at end of file 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..c5e700c77bb4 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/ChainExtensionTest.java @@ -0,0 +1,156 @@ +/* + * 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.functionaltests.AutoGeneratedUuidRecordTest.assertValidUuid; + +import java.time.Instant; +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.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.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +public class ChainExtensionTest extends LocalDynamoDbSyncTestBase { + + 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("chain-extension-table"), TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(r -> r.tableName(getConcreteTableName("chain-extension-table"))); + } + + @Test + public void putItem_appliesAllExtensions() { + Instant beforePut = Instant.now(); + MultiExtensionRecord record = new MultiExtensionRecord(); + record.setData("data"); + mappedTable.putItem(record); + + Instant afterPut = Instant.now(); + MultiExtensionRecord retrieved = mappedTable.scan().items().stream().findFirst().orElse(null); + assertThat(retrieved).isNotNull(); + + // UUID extension + assertValidUuid(retrieved.getId()); + + // Timestamp extension + assertThat(retrieved.getCreatedAt()).isNotNull(); + assertThat(retrieved.getCreatedAt()).isAfter(beforePut); + assertThat(retrieved.getCreatedAt()).isBefore(afterPut); + + // Version extension + assertThat(retrieved.getVersion()).isEqualTo(1L); + } + + @DynamoDbBean + public static class MultiExtensionRecord { + private String id; + private String data; + private Instant createdAt; + private Long version; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid + 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; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + @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; + } + MultiExtensionRecord that = (MultiExtensionRecord) o; + return Objects.equals(id, that.id) && + Objects.equals(data, that.data) && + Objects.equals(createdAt, that.createdAt) && + Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(id, data, createdAt, 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..bba5713d7739 --- /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 stringAttribute() { + 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..7515efab3eff --- /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 stringAttribute() { + 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 6e3bbdbdc9ad..3bfd47c00173 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 @@ -17,20 +17,36 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; -import org.junit.jupiter.api.Test; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext; 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 { private static final AttributeValue PARTITION_VALUE = AttributeValue.builder().s("id123").build(); private static final AttributeValue SORT_VALUE = AttributeValue.builder().s("sort123").build(); + @Mock + private TableSchema mockSchema; + @Test public void createKeyFromMap_partitionOnly() { Map itemMap = new HashMap<>(); @@ -64,4 +80,291 @@ public void cleanAttributeName_cleansSpecialCharacters() { assertThat(result).isEqualTo("a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_p_q_r_s_t_u"); } + + @Test + public void keyRef_simpleAttributeName_returnsCorrectReference() { + assertThat(EnhancedClientUtils.keyRef("simpleName")) + .isEqualTo("#AMZN_MAPPED_simpleName"); + } + + @Test + public void keyRef_attributeNameWithSpecialCharacters_returnsCleanedReference() { + assertThat(EnhancedClientUtils.keyRef("a*b.c")) + .isEqualTo("#AMZN_MAPPED_a_b_c"); + } + + @Test + public void keyRef_nestedAttributeName_returnsNestedReference() { + String result = EnhancedClientUtils.keyRef("foo.nested.bar"); + assertThat(result).contains("#AMZN_MAPPED_"); + } + + @Test + public void valueRef_simpleAttributeName_returnsCorrectReference() { + assertThat(EnhancedClientUtils.valueRef("simpleName")) + .isEqualTo(":AMZN_MAPPED_simpleName"); + } + + @Test + public void valueRef_attributeNameWithSpecialCharacters_returnsCleanedReference() { + assertThat(EnhancedClientUtils.valueRef("a*b.c")) + .isEqualTo(":AMZN_MAPPED_a_b_c"); + } + + @Test + public void valueRef_nestedAttributeName_returnsNestedReference() { + String result = EnhancedClientUtils.valueRef("foo.nested.bar"); + assertThat(result).contains(":AMZN_MAPPED_"); + } + + @Test + public void readAndTransformSingleItem_nullItemMap_returnsNull() { + assertThat( + EnhancedClientUtils.readAndTransformSingleItem( + null, + FakeItem.getTableSchema(), + null, + null)) + .isNull(); + } + + @Test + public void readAndTransformSingleItem_emptyItemMap_returnsNull() { + assertThat( + EnhancedClientUtils.readAndTransformSingleItem( + Collections.emptyMap(), + FakeItem.getTableSchema(), + null, + null)) + .isNull(); + } + + @Test + 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( + EnhancedClientUtils.readAndTransformSingleItem( + itemMap, + FakeItem.getTableSchema(), + null, + extension)) + .isNotNull(); + } + + @Test + public void readAndTransformSingleItem_withExtensionNoTransformedItem_returnsOriginalItem() { + Map itemMap = new HashMap<>(); + itemMap.put("id", PARTITION_VALUE); + DynamoDbEnhancedClientExtension extension = new DynamoDbEnhancedClientExtension() { + @Override + public ReadModification afterRead(DynamoDbExtensionContext.AfterRead context) { + return ReadModification.builder().build(); + } + }; + + 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); + } + + @Test + public void createKeyFromItem_partitionKeyOnly_returnsKeyWithPartitionOnly() { + FakeItem item = new FakeItem(); + item.setId("id123"); + + Key key = EnhancedClientUtils.createKeyFromItem( + item, + FakeItem.getTableSchema(), + TableMetadata.primaryIndexName()); + + assertThat(Objects.requireNonNull(key.partitionKeyValue()).s()).isEqualTo("id123"); + assertThat(key.sortKeyValue()).isEmpty(); + } + + @Test + public void keyRef_withSimpleKey_returnsFormattedKey() { + String result = EnhancedClientUtils.keyRef("simpleKey"); + + assertThat(result).isEqualTo("#AMZN_MAPPED_simpleKey"); + } + + @Test + public void keyRef_withSpecialCharacters_cleansAndFormatsKey() { + String result = EnhancedClientUtils.keyRef("key*with.special-chars"); + + assertThat(result).isEqualTo("#AMZN_MAPPED_key_with_special_chars"); + } + + @Test + public void keyRef_withNestedKey_handlesNestedDelimiter() { + String nestedKey = "parent_NESTED_ATTR_UPDATE_child"; + String result = EnhancedClientUtils.keyRef(nestedKey); + + assertThat(result).contains("#AMZN_MAPPED_"); + assertThat(result).contains("parent"); + assertThat(result).contains("child"); + } + + @Test + public void valueRef_withSimpleValue_returnsFormattedValue() { + String result = EnhancedClientUtils.valueRef("simpleValue"); + + assertThat(result).isEqualTo(":AMZN_MAPPED_simpleValue"); + } + + @Test + public void valueRef_withSpecialCharacters_cleansAndFormatsValue() { + String result = EnhancedClientUtils.valueRef("value*with.special-chars"); + + assertThat(result).isEqualTo(":AMZN_MAPPED_value_with_special_chars"); + } + + @Test + public void valueRef_withNestedValue_handlesNestedDelimiter() { + String nestedValue = "parent_NESTED_ATTR_UPDATE_child"; + String result = EnhancedClientUtils.valueRef(nestedValue); + + assertThat(result).startsWith(":AMZN_MAPPED_"); + assertThat(result).contains("parent"); + assertThat(result).contains("child"); + } + + @Test + public void cleanAttributeName_withNoSpecialCharacters_returnsOriginal() { + String original = "normalAttributeName123"; + String result = EnhancedClientUtils.cleanAttributeName(original); + + assertThat(result).isSameAs(original); // Should return same instance when no changes needed + } + + @Test + public void isNullAttributeValue_withNullAttributeValue_returnsTrue() { + AttributeValue nullValue = AttributeValue.builder().nul(true).build(); + + boolean result = EnhancedClientUtils.isNullAttributeValue(nullValue); + + assertThat(result).isTrue(); + } + + @Test + public void isNullAttributeValue_withNonNullAttributeValue_returnsFalse() { + AttributeValue stringValue = AttributeValue.builder().s("test").build(); + + boolean result = EnhancedClientUtils.isNullAttributeValue(stringValue); + + assertThat(result).isFalse(); + } + + @Test + public void isNullAttributeValue_withFalseNullValue_returnsFalse() { + AttributeValue falseNullValue = AttributeValue.builder().nul(false).build(); + + boolean result = EnhancedClientUtils.isNullAttributeValue(falseNullValue); + + assertThat(result).isFalse(); + } + + @Test + public void createKeyFromItem_withPartitionKeyOnly_createsCorrectKey() { + FakeItem item = new FakeItem(); + item.setId("test-id"); + + Key result = EnhancedClientUtils.createKeyFromItem(item, FakeItem.getTableSchema(), + TableMetadata.primaryIndexName()); + + assertThat(result.partitionKeyValue()).isEqualTo(AttributeValue.builder().s("test-id").build()); + assertThat(result.sortKeyValue()).isEmpty(); + } + + @Test + public void createKeyFromItem_withPartitionAndSortKey_createsCorrectKey() { + FakeItemWithSort item = new FakeItemWithSort(); + item.setId("test-id"); + item.setSort("test-sort"); + + Key result = EnhancedClientUtils.createKeyFromItem(item, FakeItemWithSort.getTableSchema(), + TableMetadata.primaryIndexName()); + + assertThat(result.partitionKeyValue()).isEqualTo(AttributeValue.builder().s("test-id").build()); + assertThat(result.sortKeyValue()).isPresent(); + assertThat(result.sortKeyValue().get()).isEqualTo(AttributeValue.builder().s("test-sort").build()); + } + + @Test + public void readAndTransformSingleItem_withNullItemMap_returnsNull() { + Object result = EnhancedClientUtils.readAndTransformSingleItem(null, mockSchema, null, null); + + assertThat(result).isNull(); + } + + @Test + public void readAndTransformSingleItem_withEmptyItemMap_returnsNull() { + Map emptyMap = Collections.emptyMap(); + + Object result = EnhancedClientUtils.readAndTransformSingleItem(emptyMap, mockSchema, null, null); + + assertThat(result).isNull(); + } + + @Test + public void getItemsFromSupplier_withNullList_returnsNull() { + List result = EnhancedClientUtils.getItemsFromSupplier(null); + + assertThat(result).isNull(); + } + + @Test + public void getItemsFromSupplier_withEmptyList_returnsNull() { + List result = EnhancedClientUtils.getItemsFromSupplier(Collections.emptyList()); + + assertThat(result).isNull(); + } } \ No newline at end of file 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..a8466e55fa2f 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 @@ -68,6 +68,7 @@ public void index_constructsCorrectMappedIndex() { assertThat(dynamoDbMappedIndex.dynamoDbClient(), is(sameInstance(mockDynamoDbAsyncClient))); assertThat(dynamoDbMappedIndex.mapperExtension(), is(sameInstance(mockDynamoDbEnhancedClientExtension))); assertThat(dynamoDbMappedIndex.tableSchema(), is(sameInstance(FakeItemWithIndices.getTableSchema()))); + assertThat(dynamoDbMappedIndex.tableName(), is(TABLE_NAME)); assertThat(dynamoDbMappedIndex.indexName(), is("gsi_1")); } @@ -169,6 +170,7 @@ public void createTable_groupsSecondaryIndexesExistingInTableSchema() { assertThat(request.localSecondaryIndexes().size(), is(1)); Iterator lsiIterator = request.localSecondaryIndexes().iterator(); + assertThat(dynamoDbMappedIndex.tableName(), is("test_table")); assertThat(lsiIterator.next().indexName(), is("lsi_1")); assertThat(request.globalSecondaryIndexes().size(), is(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/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..0f3d1d0bef15 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 @@ -20,12 +20,14 @@ import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.stream.Collectors.toList; +import static org.hamcrest.CoreMatchers.containsString; 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.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doReturn; @@ -41,12 +43,14 @@ 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.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +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,12 +63,14 @@ 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) public class BatchGetItemOperationTest { @@ -107,6 +113,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(), is("BatchGetItem")); + } + @Test public void getServiceCall_usingShortcutForm_makesTheRightCallAndReturnsResponse() { BatchGetItemEnhancedRequest batchGetItemEnhancedRequest = @@ -381,14 +393,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)) )); }); @@ -413,6 +427,219 @@ public void transformResponse_queryingEmptyResults() { assertThat(resultsPage.resultsForTable(fakeItemMappedTable), is(emptyList())); } + @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, containsInAnyOrder( + 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(), nullValue()); + } + + @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, containsInAnyOrder( + 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(), is(true)); + } + + @Test + public void generateRequest_mergesKeysAndAttributes_incompatibleConsistentRead_throws() { + 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(false) + .build()) + .build(); + BatchGetItemEnhancedRequest req = BatchGetItemEnhancedRequest.builder() + .readBatches(batch1, batch2) + .build(); + BatchGetItemOperation op = BatchGetItemOperation.create(req); + + try { + op.generateRequest(null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("same 'consistentRead' setting")); + } + } + + @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_throwsIfFirstBatchTrueSecondBatchNullConsistentRead() { + ReadBatch batch1 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(GetItemEnhancedRequest.builder().key(Key.builder().partitionValue("3").build()).consistentRead(true).build()) + .build(); + ReadBatch batch2 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(Key.builder().partitionValue("4").build()) + .build(); + BatchGetItemEnhancedRequest request = BatchGetItemEnhancedRequest.builder() + .readBatches(batch1, batch2) + .build(); + BatchGetItemOperation operation = BatchGetItemOperation.create(request); + try { + operation.generateRequest(null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("same 'consistentRead' setting")); + } + } + + @Test + public void generateRequest_throwsIfFirstBatchNullSecondBatchFalseConsistentRead() { + ReadBatch batch1 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(Key.builder().partitionValue("5").build()) + .build(); + ReadBatch batch2 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(GetItemEnhancedRequest.builder().key(Key.builder().partitionValue("6").build()).consistentRead(false).build()) + .build(); + BatchGetItemEnhancedRequest request = BatchGetItemEnhancedRequest.builder() + .readBatches(batch1, batch2) + .build(); + BatchGetItemOperation operation = BatchGetItemOperation.create(request); + try { + operation.generateRequest(null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("same 'consistentRead' setting")); + } + } + + @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 generateRequest_throwsIfBatchesHaveDifferentNonNullConsistentReadValues() { + ReadBatch batch1 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(GetItemEnhancedRequest.builder().key(Key.builder().partitionValue("9").build()).consistentRead(true).build()) + .build(); + ReadBatch batch2 = ReadBatch.builder(FakeItem.class) + .mappedTableResource(fakeItemMappedTable) + .addGetItem(GetItemEnhancedRequest.builder().key(Key.builder().partitionValue("10").build()).consistentRead(false).build()) + .build(); + BatchGetItemEnhancedRequest request = BatchGetItemEnhancedRequest.builder() + .readBatches(batch1, batch2) + .build(); + BatchGetItemOperation operation = BatchGetItemOperation.create(request); + try { + operation.generateRequest(null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("same 'consistentRead' setting")); + } + } + + @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, is(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, is(mockPublisher)); + verify(mockAsyncClient).batchGetItemPaginator(any(BatchGetItemRequest.class)); + } + private static BatchGetItemEnhancedRequest emptyRequest() { return BatchGetItemEnhancedRequest.builder().readBatches().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..dd2b187d7294 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 @@ -112,6 +112,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(), is("BatchWriteItem")); + } + @Test public void getServiceCall_makesTheRightCallAndReturnsResponse() { @@ -221,15 +237,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)) )); }); @@ -350,14 +368,16 @@ 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)) )); }); 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..48fe6c2b9824 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 @@ -117,6 +117,14 @@ public void describeTo(Description description) { } } + @Test + public void returnsCorrectOperationName() { + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder().build()); + + assertThat(operation.operationName().label(), is("CreateTable")); + } + @Test public void generateRequest_withLsiAndGsi() { Projection projection1 = Projection.builder().projectionType(ProjectionType.ALL).build(); 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..1cfdf3c661ee 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 @@ -93,6 +93,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(), is("DeleteItem")); + } + @Test public void getServiceCall_makesTheRightCallAndReturnsResponse() { FakeItem keyItem = createUniqueFakeItem(); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/DeleteTableOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/DeleteTableOperationTest.java index 234eaf446206..428853adfa10 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/DeleteTableOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/DeleteTableOperationTest.java @@ -15,6 +15,11 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.verify; + import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -24,15 +29,9 @@ import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithIndices; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort; -import software.amazon.awssdk.enhanced.dynamodb.model.CreateTableEnhancedRequest; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.verify; - @RunWith(MockitoJUnitRunner.class) public class DeleteTableOperationTest { @@ -46,6 +45,11 @@ public class DeleteTableOperationTest { @Mock private DynamoDbClient mockDynamoDbClient; + @Test + public void returnsCorrectOperationName() { + DeleteTableOperation operation = DeleteTableOperation.create(); + assertThat(operation.operationName().label(), is("DeleteItem")); + } @Test public void getServiceCall_makesTheRightCall() { 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..7172b38545ce 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 @@ -46,6 +46,12 @@ public class DescribeTableOperationTest { @Mock private DynamoDbClient mockDynamoDbClient; + @Test + public void returnsCorrectOperationName() { + DescribeTableOperation operation = DescribeTableOperation.create(); + assertThat(operation.operationName().label(), is("DescribeTable")); + } + @Test public void getServiceCall_makesTheRightCall() { DescribeTableOperation operation = DescribeTableOperation.create(); 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..44ce7d454095 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 @@ -67,6 +67,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(), is("GetItem")); + } + @Test public void getServiceCall_makesTheRightCallAndReturnsResponse() { FakeItem keyItem = createUniqueFakeItem(); 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..ee0d89e7c8e2 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 @@ -94,6 +94,11 @@ public class QueryOperationTest { @Mock private DynamoDbEnhancedClientExtension mockDynamoDbEnhancedClientExtension; + @Test + public void returnsCorrectOperationName() { + assertThat(queryOperation.operationName().label(), is("Query")); + } + @Test public void getServiceCall_makesTheRightCallAndReturnsResponse() { QueryRequest queryRequest = QueryRequest.builder().build(); 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..3eb93c8868ec 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 @@ -103,6 +103,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(), is("TransactGetItems")); + } + @Test public void generateRequest_getsFromMultipleTables_usingShortcutForm() { TransactGetItemsEnhancedRequest transactGetItemsEnhancedRequest = 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..0fd5093ab74a 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 @@ -83,6 +83,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(), is("TransactWriteItems")); + } + @Test public void generateRequest_singleTransaction() { TransactWriteItemsEnhancedRequest transactGetItemsEnhancedRequest = 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..37bc52cd0e11 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 @@ -22,8 +22,8 @@ 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.is; 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; @@ -35,18 +35,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 +60,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 +75,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 +94,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) @@ -1428,6 +1432,19 @@ public void rootSchema_areCached_but_flattenedAreNot() { assertThat(root1, not(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 public static class MultipleFlattenMapsBean { private String id; 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..b74b643dea81 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 @@ -814,6 +814,53 @@ public void itemType_returnsCorrectClassWhenBuiltWithEnhancedType() { assertThat(tableSchema.itemType(), is(equalTo(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(), is(2)); + assertThat(result, hasEntry("attr1", AttributeValue.builder().s("test").build())); + assertThat(result, hasEntry("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(), is(1)); + assertThat(result, hasEntry("attr", AttributeValue.builder().s("test").build())); + } + @Test public void getTableMetadata_hasCorrectFields() { TableMetadata tableMetadata = FakeItemWithSort.getTableSchema().tableMetadata(); @@ -1360,7 +1407,7 @@ public void buildAbstractExtends() { } @Test - public void buildAbstractTagWith() { + public void buildAbstractTagsWith() { StaticTableSchema abstractTableSchema = StaticTableSchema @@ -1373,7 +1420,7 @@ public void buildAbstractTagWith() { } @Test - public void buildConcreteTagWith() { + public void buildConcreteTagsWith() { StaticTableSchema concreteTableSchema = StaticTableSchema @@ -1386,6 +1433,58 @@ public void buildConcreteTagWith() { is(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), + is(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), + is(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), + is(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), + is(Optional.of(TABLE_TAG_VALUE))); + } + @Test public void instantiateFlattenedAbstractClassShouldThrowException() { StaticTableSchema superclassTableSchema = 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