-
Notifications
You must be signed in to change notification settings - Fork 1k
Feature/dynamodb enhanced increase functional tests coverage #6946
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "type": "bugfix", | ||
| "category": "Amazon DynamoDB Enhanced Client", | ||
| "contributor": "", | ||
| "description": "Fix AutoGeneratedTimestampRecordExtension failing when TableSchema.converterForAttribute throws UnsupportedOperationException for custom schemas; expand dynamodb-enhanced functional test coverage" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| /* | ||
| * 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 static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.function.Supplier; | ||
| import java.util.stream.Stream; | ||
| import org.junit.jupiter.params.ParameterizedTest; | ||
| import org.junit.jupiter.params.provider.Arguments; | ||
| import org.junit.jupiter.params.provider.MethodSource; | ||
| import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; | ||
| import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; | ||
| import software.amazon.awssdk.enhanced.dynamodb.TableSchema; | ||
| import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext; | ||
| import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext; | ||
| import software.amazon.awssdk.enhanced.dynamodb.internal.operations.OperationName; | ||
| import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; | ||
| import software.amazon.awssdk.services.dynamodb.model.AttributeValue; | ||
|
|
||
| public class ExtensionSpiContractTest { | ||
|
|
||
| private static final String TABLE_NAME = "extension-spi-contract-table"; | ||
|
|
||
| private static final TableSchema<NoExtensionItem> TABLE_SCHEMA = StaticTableSchema.builder(NoExtensionItem.class) | ||
| .newItemSupplier(NoExtensionItem::new) | ||
| .addAttribute(String.class, | ||
| a -> a.name("id") | ||
| .getter(NoExtensionItem::getId) | ||
| .setter(NoExtensionItem::setId) | ||
| .tags(primaryPartitionKey())) | ||
| .build(); | ||
|
|
||
| private static final TableMetadata TABLE_METADATA = TABLE_SCHEMA.tableMetadata(); | ||
|
|
||
| private static final List<OperationName> WRITE_OPERATIONS = Arrays.asList( | ||
| OperationName.PUT_ITEM, | ||
| OperationName.UPDATE_ITEM, | ||
| OperationName.BATCH_WRITE_ITEM, | ||
| OperationName.TRANSACT_WRITE_ITEMS | ||
| ); | ||
|
|
||
| private static Stream<Arguments> extensionAndOperations() { | ||
| List<Supplier<DynamoDbEnhancedClientExtension>> extensionSuppliers = Arrays.asList( | ||
| AutoGeneratedTimestampRecordExtension::create, | ||
| AutoGeneratedUuidExtension::create, | ||
| () -> AtomicCounterExtension.builder().build(), | ||
| () -> VersionedRecordExtension.builder().build() | ||
| ); | ||
|
|
||
| return extensionSuppliers.stream() | ||
| .flatMap(supplier -> WRITE_OPERATIONS.stream() | ||
| .map(operation -> Arguments.of(supplier.get(), | ||
| operation))); | ||
| } | ||
|
|
||
| @ParameterizedTest(name = "{0} with {1}") | ||
| @MethodSource("extensionAndOperations") | ||
| public void beforeWrite_noOptionalCapabilities_returnsNoOpAndDoesNotMutateItems( | ||
| DynamoDbEnhancedClientExtension extension, | ||
| OperationName operationName) { | ||
|
|
||
| Map<String, AttributeValue> items = new HashMap<>(); | ||
| items.put("id", AttributeValue.builder().s("id1").build()); | ||
|
|
||
| Map<String, AttributeValue> originalItems = new HashMap<>(items); | ||
|
|
||
| DefaultDynamoDbExtensionContext context = DefaultDynamoDbExtensionContext.builder() | ||
| .items(items) | ||
| .tableMetadata(TABLE_METADATA) | ||
| .tableSchema(TABLE_SCHEMA) | ||
| .operationName(operationName) | ||
| .operationContext(DefaultOperationContext.create(TABLE_NAME)) | ||
| .build(); | ||
|
|
||
| WriteModification modification = extension.beforeWrite(context); | ||
|
|
||
| assertThat(items).isEqualTo(originalItems); | ||
| assertThat(modification).isEqualTo(WriteModification.builder().build()); | ||
| } | ||
|
|
||
| private static final class NoExtensionItem { | ||
| private String id; | ||
|
|
||
| public String getId() { | ||
| return id; | ||
| } | ||
|
|
||
| public void setId(String id) { | ||
| this.id = id; | ||
| } | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.functionaltests; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: This file, along with other new ones are missing the copyright header.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added the standard Apache 2.0 copyright header to the new async test files called out here, including AsyncAtomicCounterTest, AsyncAutoGeneratedUuidRecordTest, AsyncUpdateBehaviorTest, AsyncUpdateExpressionTest, and AsyncVersionedRecordTest. |
||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import org.junit.After; | ||
| import org.junit.Before; | ||
| import org.junit.Test; | ||
| import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; | ||
| import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; | ||
| import software.amazon.awssdk.enhanced.dynamodb.TableSchema; | ||
| import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AtomicCounterRecord; | ||
|
|
||
| public class AsyncAtomicCounterTest extends LocalDynamoDbAsyncTestBase { | ||
| private static final TableSchema<AtomicCounterRecord> TABLE_SCHEMA = TableSchema.fromClass(AtomicCounterRecord.class); | ||
|
|
||
| private final DynamoDbEnhancedAsyncClient enhancedAsyncClient = | ||
| DynamoDbEnhancedAsyncClient.builder().dynamoDbClient(getDynamoDbAsyncClient()).build(); | ||
|
|
||
| private final DynamoDbAsyncTable<AtomicCounterRecord> mappedTable = | ||
| enhancedAsyncClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); | ||
|
|
||
| @Before | ||
| public void createTable() { | ||
| mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); | ||
| } | ||
|
|
||
| @After | ||
| public void deleteTable() { | ||
| getDynamoDbAsyncClient().deleteTable(r -> r.tableName(getConcreteTableName("table-name"))).join(); | ||
| } | ||
|
|
||
| @Test | ||
| public void repeatedUpdate_shouldIncrementCountersOnEachUpdate() { | ||
| AtomicCounterRecord record = new AtomicCounterRecord(); | ||
| record.setId("id1"); | ||
| record.setAttribute1("value"); | ||
| mappedTable.updateItem(record).join(); | ||
| mappedTable.updateItem(record).join(); | ||
| mappedTable.updateItem(record).join(); | ||
|
|
||
| AtomicCounterRecord persisted = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id1"))).join(); | ||
| // AtomicCounterRecord annotations: defaultCounter (delta=1, start=0), customCounter (delta=5, start=10), | ||
| // decreasingCounter (delta=-1, start=-20). First updateItem creates the item using start values; | ||
| // each subsequent update adds delta. After 3 updateItem calls: 0+1+1=2, 10+5+5=20, -20-1-1=-22. | ||
| assertThat(persisted.getDefaultCounter()).isEqualTo(2L); | ||
| assertThat(persisted.getCustomCounter()).isEqualTo(20L); | ||
| assertThat(persisted.getDecreasingCounter()).isEqualTo(-22L); | ||
|
Comment on lines
+60
to
+62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These feel like magic numbers. Where do they come from? We should at least comment how they were derived so that when they fail in the future we can understand why
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a comment explaining how the expected counter values are derived from the AtomicCounterRecord annotation defaults (start / delta) across the three updateItem calls. |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| /* | ||
| * 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.hamcrest.MatcherAssert.assertThat; | ||
| import static org.hamcrest.Matchers.is; | ||
| import static org.hamcrest.Matchers.notNullValue; | ||
|
|
||
| import java.time.Clock; | ||
| import java.time.Instant; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import org.junit.After; | ||
| import org.junit.Before; | ||
| import org.junit.Test; | ||
| import org.mockito.Mockito; | ||
| import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; | ||
| import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; | ||
| import software.amazon.awssdk.enhanced.dynamodb.TableSchema; | ||
| import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; | ||
| import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.BeanWithCustomConvertedList; | ||
| import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.CustomConvertedPojo; | ||
| import software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbEnhancedAsyncClient; | ||
| import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; | ||
| import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; | ||
| import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; | ||
| import software.amazon.awssdk.enhanced.dynamodb.model.BatchWriteItemEnhancedRequest; | ||
| import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; | ||
|
|
||
| public class AsyncAutoGeneratedTimestampConvertedByListTest extends LocalDynamoDbAsyncTestBase { | ||
| private static final Instant MOCKED_INSTANT_NOW = Instant.parse("2019-01-13T14:00:00Z"); | ||
|
|
||
| private DynamoDbEnhancedAsyncClient extensionEnhancedAsyncClient; | ||
| private DynamoDbAsyncTable<BeanWithCustomConvertedList> table; | ||
| private String tableName; | ||
|
|
||
| private final TableSchema<BeanWithCustomConvertedList> tableSchema = | ||
| BeanTableSchema.create(BeanWithCustomConvertedList.class); | ||
|
|
||
| private Clock mockClock; | ||
|
|
||
| @Before | ||
| public void setUp() { | ||
| mockClock = Mockito.mock(Clock.class); | ||
| Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_NOW); | ||
|
|
||
| extensionEnhancedAsyncClient = | ||
| DefaultDynamoDbEnhancedAsyncClient.builder() | ||
| .dynamoDbClient(getDynamoDbAsyncClient()) | ||
| .extensions(AutoGeneratedTimestampRecordExtension.builder() | ||
| .baseClock(mockClock) | ||
| .build()) | ||
| .build(); | ||
|
|
||
| tableName = getConcreteTableName("converted-by-list-extension-async-test-table"); | ||
| table = extensionEnhancedAsyncClient.table(tableName, tableSchema); | ||
| table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); | ||
| } | ||
|
|
||
| @After | ||
| public void tearDown() { | ||
| getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() | ||
| .tableName(tableName) | ||
| .build()).join(); | ||
| } | ||
|
|
||
| @Test | ||
| public void putThenUpdateItem_customConvertedByList_doesNotThrow_setsTimestampAndPreservesCustomItems() { | ||
| BeanWithCustomConvertedList initial = new BeanWithCustomConvertedList() | ||
| .setId("id1") | ||
| .setCustomItems(Arrays.asList( | ||
| new CustomConvertedPojo("a", 1), | ||
| new CustomConvertedPojo("b", 2))); | ||
|
|
||
| table.putItem(r -> r.item(initial)).join(); | ||
|
|
||
| List<CustomConvertedPojo> updatedCustomItems = Arrays.asList( | ||
| new CustomConvertedPojo("c", 3), | ||
| new CustomConvertedPojo("d", 4)); | ||
|
|
||
| BeanWithCustomConvertedList updated = new BeanWithCustomConvertedList() | ||
| .setId("id1") | ||
| .setCustomItems(updatedCustomItems); | ||
|
|
||
| table.updateItem(r -> r.item(updated)).join(); | ||
|
|
||
| BeanWithCustomConvertedList result = | ||
| table.getItem(r -> r.key(k -> k.partitionValue("id1"))).join(); | ||
|
|
||
| assertThat(result, is(notNullValue())); | ||
| assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); | ||
| assertCustomItems(result.getCustomItems(), updatedCustomItems); | ||
| } | ||
|
|
||
| @Test | ||
| public void batchWriteItem_customConvertedByList_doesNotThrow_setsTimestampAndPreservesCustomItems() { | ||
| BeanWithCustomConvertedList record = new BeanWithCustomConvertedList() | ||
| .setId("id2") | ||
| .setCustomItems(Arrays.asList( | ||
| new CustomConvertedPojo("a", 1), | ||
| new CustomConvertedPojo("b", 2))); | ||
|
|
||
| WriteBatch writeBatch = WriteBatch.builder(BeanWithCustomConvertedList.class) | ||
| .mappedTableResource(table) | ||
| .addPutItem(r -> r.item(record)) | ||
| .build(); | ||
|
|
||
| extensionEnhancedAsyncClient.batchWriteItem( | ||
| BatchWriteItemEnhancedRequest.builder() | ||
| .writeBatches(Arrays.asList(writeBatch)) | ||
| .build()).join(); | ||
|
|
||
| BeanWithCustomConvertedList result = | ||
| table.getItem(r -> r.key(k -> k.partitionValue("id2"))).join(); | ||
|
|
||
| assertThat(result, is(notNullValue())); | ||
| assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); | ||
| assertCustomItems(result.getCustomItems(), record.getCustomItems()); | ||
| } | ||
|
|
||
| @Test | ||
| public void transactWriteItems_customConvertedByList_doesNotThrow_setsTimestampAndPreservesCustomItems() { | ||
| BeanWithCustomConvertedList record = new BeanWithCustomConvertedList() | ||
| .setId("id3") | ||
| .setCustomItems(Arrays.asList( | ||
| new CustomConvertedPojo("a", 1), | ||
| new CustomConvertedPojo("b", 2))); | ||
|
|
||
| extensionEnhancedAsyncClient.transactWriteItems( | ||
| TransactWriteItemsEnhancedRequest.builder() | ||
| .addPutItem(table, record) | ||
| .build()).join(); | ||
|
|
||
| BeanWithCustomConvertedList result = | ||
| table.getItem(r -> r.key(k -> k.partitionValue("id3"))).join(); | ||
|
|
||
| assertThat(result, is(notNullValue())); | ||
| assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); | ||
| assertCustomItems(result.getCustomItems(), record.getCustomItems()); | ||
| } | ||
|
|
||
| private static void assertCustomItems(List<CustomConvertedPojo> actual, | ||
| List<CustomConvertedPojo> expected) { | ||
| assertThat(actual.size(), is(expected.size())); | ||
| for (int i = 0; i < expected.size(); i++) { | ||
| assertThat(actual.get(i).getLabel(), is(expected.get(i).getLabel())); | ||
| assertThat(actual.get(i).getCount(), is(expected.get(i).getCount())); | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do the test cases actually cover this? When I removed this check, all the tests still passed which implies we don't actually have sufficient coverage of a nested schema case that hits this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added coverage specifically for nested schemas that throw UnsupportedOperationException from converterForAttribute. This is now exercised by AutoGeneratedTimestampExtensionTest#beforeWrite_nestedSchemaWithoutConverterForAttribute_doesNotThrow_nestedTimestampsSkipped, which verifies the write does not fail and nested timestamp insertion is skipped.