diff --git a/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml b/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml
index 28f3a6bf6..d5e31d255 100644
--- a/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml
+++ b/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml
@@ -77,6 +77,16 @@
junit-jupiter-engine
test
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
org.slf4j
slf4j-simple
@@ -89,15 +99,6 @@
test-jar
test
-
-
- com.amazonaws
- DynamoDBLocal
-
-
- 2.2.0
- test
-
software.amazon.awssdk
s3
@@ -125,6 +126,7 @@
org.graalvm.buildtools
native-maven-plugin
+ true
powertools-idempotency-dynamodb
@@ -149,57 +151,6 @@
-
-
- org.apache.maven.plugins
- maven-dependency-plugin
-
-
- copy-dynamodb-local
- generate-test-resources
-
- copy-dependencies
-
-
- test
- false
-
- software.amazon.lambda
- ${project.build.directory}/dynamodb-local
-
-
-
-
-
-
- org.codehaus.mojo
- exec-maven-plugin
-
-
- start-dynamodb-local
- process-test-classes
-
- exec
-
-
- java
- ${project.build.directory}/dynamodb-local
-
- -Djava.library.path=${project.build.directory}/dynamodb-local
- -cp
- ${project.build.directory}/dynamodb-local/*
- com.amazonaws.services.dynamodbv2.local.main.ServerRunner
- -inMemory
- -port
- 8000
-
- true
- true
-
-
-
-
org.apache.maven.plugins
maven-jar-plugin
@@ -212,16 +163,6 @@
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
-
-
- http://localhost:8000
-
-
-
dev.aspectj
aspectj-maven-plugin
diff --git a/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBConfig.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBConfig.java
deleted file mode 100644
index 9f6875689..000000000
--- a/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBConfig.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2023 Amazon.com, Inc. or its affiliates.
- * Licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package software.amazon.lambda.powertools.idempotency.persistence.dynamodb;
-
-import java.net.URI;
-
-import org.junit.jupiter.api.BeforeAll;
-
-import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
-import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
-import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
-import software.amazon.awssdk.regions.Region;
-import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
-import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
-import software.amazon.awssdk.services.dynamodb.model.BillingMode;
-import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
-import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
-import software.amazon.awssdk.services.dynamodb.model.KeyType;
-import software.amazon.awssdk.services.dynamodb.model.ResourceInUseException;
-import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
-
-class DynamoDBConfig {
- protected static final String TABLE_NAME = "idempotency_table";
- protected static DynamoDbClient client;
-
- @BeforeAll
- static void setupDynamo() {
- String endpoint = System.getProperty("dynamodb.endpoint", "http://localhost:8000");
-
- client = DynamoDbClient.builder()
- .httpClient(UrlConnectionHttpClient.builder().build())
- .region(Region.EU_WEST_1)
- .endpointOverride(URI.create(endpoint))
- .credentialsProvider(StaticCredentialsProvider.create(
- AwsBasicCredentials.create("FAKE", "FAKE")))
- .build();
-
- try {
- client.createTable(CreateTableRequest.builder()
- .tableName(TABLE_NAME)
- .keySchema(KeySchemaElement.builder().keyType(KeyType.HASH).attributeName("id").build())
- .attributeDefinitions(
- AttributeDefinition.builder().attributeName("id").attributeType(ScalarAttributeType.S)
- .build())
- .billingMode(BillingMode.PAY_PER_REQUEST)
- .build());
- } catch (ResourceInUseException e) {
- // Table already exists, ignore
- } catch (Exception e) {
- throw new RuntimeException("Failed to create DynamoDB table", e);
- }
- }
-}
diff --git a/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java
index b5c816286..6d4fb8d85 100644
--- a/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java
+++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java
@@ -16,30 +16,30 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
-import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import java.util.OptionalLong;
-import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junitpioneer.jupiter.SetEnvironmentVariable;
+import org.mockito.ArgumentCaptor;
-import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
-import software.amazon.awssdk.services.dynamodb.model.BillingMode;
-import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
+import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
-import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
-import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
-import software.amazon.awssdk.services.dynamodb.model.KeyType;
+import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
-import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
-import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
+import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
import software.amazon.lambda.powertools.idempotency.Constants;
import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
@@ -47,342 +47,293 @@
import software.amazon.lambda.powertools.idempotency.persistence.DataRecord;
/**
- * These test are using DynamoDBLocal and sqlite, see https://nickolasfisher.com/blog/Configuring-an-In-Memory-DynamoDB-instance-with-Java-for-Integration-Testing
- * NOTE: on a Mac with Apple Chipset, you need to use the Oracle JDK x86 64-bit
+ * Unit tests for DynamoDBPersistenceStore using mocked DynamoDbClient.
*/
-class DynamoDBPersistenceStoreTest extends DynamoDBConfig {
- protected static final String TABLE_NAME_CUSTOM = "idempotency_table_custom";
- private Map key;
- private DynamoDBPersistenceStore dynamoDBPersistenceStore;
+class DynamoDBPersistenceStoreTest {
+ private static final String TABLE_NAME = "idempotency_table";
+ private DynamoDbClient mockClient;
+ private DynamoDBPersistenceStore persistenceStore;
+
+ @BeforeEach
+ void setup() {
+ mockClient = mock(DynamoDbClient.class);
+ persistenceStore = DynamoDBPersistenceStore.builder()
+ .withTableName(TABLE_NAME)
+ .withDynamoDbClient(mockClient)
+ .build();
+ }
@Test
- void putRecord_shouldCreateRecordInDynamoDB() throws IdempotencyItemAlreadyExistsException {
+ void putRecord_shouldSendCorrectPutItemRequest() throws IdempotencyItemAlreadyExistsException {
+ // GIVEN
Instant now = Instant.now();
long expiry = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond();
- dynamoDBPersistenceStore.putRecord(new DataRecord("key", DataRecord.Status.COMPLETED, expiry, null, null), now);
-
- key = Collections.singletonMap("id", AttributeValue.builder().s("key").build());
- Map item = client
- .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
- assertThat(item).isNotNull();
- assertThat(item.get("status").s()).isEqualTo("COMPLETED");
- assertThat(item.get("expiration").n()).isEqualTo(String.valueOf(expiry));
- }
+ DataRecord dataRecord = new DataRecord("key", DataRecord.Status.COMPLETED, expiry, null, null);
- @Test
- void putRecord_shouldCreateRecordInDynamoDB_IfPreviousExpired() {
- key = Collections.singletonMap("id", AttributeValue.builder().s("key").build());
+ // WHEN
+ persistenceStore.putRecord(dataRecord, now);
- // GIVEN: Insert a fake item with same id and expired
- Map item = new HashMap<>(key);
- Instant now = Instant.now();
- long expiry = now.minus(30, ChronoUnit.SECONDS).getEpochSecond();
- item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build());
- item.put("status", AttributeValue.builder().s(DataRecord.Status.COMPLETED.toString()).build());
- item.put("data", AttributeValue.builder().s("Fake Data").build());
- client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build());
-
- // WHEN: call putRecord
- long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond();
- dynamoDBPersistenceStore.putRecord(
- new DataRecord("key",
- DataRecord.Status.INPROGRESS,
- expiry2,
- null,
- null),
- now);
-
- // THEN: an item is inserted
- Map itemInDb = client
- .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
- assertThat(itemInDb).isNotNull();
- assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS");
- assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry2));
+ // THEN
+ ArgumentCaptor captor = ArgumentCaptor.forClass(PutItemRequest.class);
+ verify(mockClient).putItem(captor.capture());
+
+ PutItemRequest request = captor.getValue();
+ assertThat(request.tableName()).isEqualTo(TABLE_NAME);
+ assertThat(request.item()).containsEntry("id", AttributeValue.builder().s("key").build());
+ assertThat(request.item().get("status").s()).isEqualTo("COMPLETED");
+ assertThat(request.item().get("expiration").n()).isEqualTo(String.valueOf(expiry));
+ assertThat(request.conditionExpression()).contains("attribute_not_exists(#id)");
+ assertThat(request.conditionExpression()).contains("#expiry < :now");
}
@Test
- void putRecord_shouldCreateRecordInDynamoDB_IfLambdaWasInProgressAndTimedOut() {
- key = Collections.singletonMap("id", AttributeValue.builder().s("key").build());
-
- // GIVEN: Insert a fake item with same id and progress expired (Lambda timed out before and we allow a new
- // execution)
- Map item = new HashMap<>(key);
+ void putRecord_shouldIncludeInProgressExpiry_whenProvided() throws IdempotencyItemAlreadyExistsException {
+ // GIVEN
Instant now = Instant.now();
- long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond();
- long progressExpiry = now.minus(30, ChronoUnit.SECONDS).toEpochMilli();
- item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build());
- item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build());
- item.put("data", AttributeValue.builder().s("Fake Data").build());
- item.put("in_progress_expiration", AttributeValue.builder().n(String.valueOf(progressExpiry)).build());
- client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build());
-
- // WHEN: call putRecord
- long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond();
- dynamoDBPersistenceStore.putRecord(
- new DataRecord("key",
- DataRecord.Status.INPROGRESS,
- expiry2,
- null,
- null),
- now);
-
- // THEN: an item is inserted
- Map itemInDb = client
- .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
- assertThat(itemInDb).isNotNull();
- assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS");
- assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry2));
+ long expiry = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond();
+ long inProgressExpiry = now.plus(300, ChronoUnit.SECONDS).toEpochMilli();
+ DataRecord dataRecord = new DataRecord("key", DataRecord.Status.INPROGRESS, expiry, null, null, OptionalLong.of(inProgressExpiry));
+
+ // WHEN
+ persistenceStore.putRecord(dataRecord, now);
+
+ // THEN
+ ArgumentCaptor captor = ArgumentCaptor.forClass(PutItemRequest.class);
+ verify(mockClient).putItem(captor.capture());
+
+ PutItemRequest request = captor.getValue();
+ assertThat(request.item().get("in_progress_expiration").n()).isEqualTo(String.valueOf(inProgressExpiry));
}
@Test
- void putRecord_shouldThrowIdempotencyItemAlreadyExistsException_IfRecordAlreadyExist() {
- key = Collections.singletonMap("id", AttributeValue.builder().s("key").build());
-
- // GIVEN: Insert a fake item with same id
- Map item = new HashMap<>(key);
+ void putRecord_shouldThrowIdempotencyItemAlreadyExistsException_whenConditionFails() {
+ // GIVEN
Instant now = Instant.now();
- long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond();
- item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build()); // not expired
- item.put("status", AttributeValue.builder().s(DataRecord.Status.COMPLETED.toString()).build());
- item.put("data", AttributeValue.builder().s("Fake Data").build());
- client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build());
-
- // WHEN: call putRecord
- long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond();
- DataRecord recordToInsert = new DataRecord("key",
- DataRecord.Status.INPROGRESS,
- expiry2,
- null,
- null);
- assertThatThrownBy(() -> dynamoDBPersistenceStore.putRecord(recordToInsert, now))
+ long expiry = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond();
+ DataRecord dataRecord = new DataRecord("key", DataRecord.Status.INPROGRESS, expiry, null, null);
+
+ Map existingItem = new HashMap<>();
+ existingItem.put("id", AttributeValue.builder().s("key").build());
+ existingItem.put("status", AttributeValue.builder().s("COMPLETED").build());
+ existingItem.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build());
+ existingItem.put("data", AttributeValue.builder().s("Existing Data").build());
+
+ ConditionalCheckFailedException exception = ConditionalCheckFailedException.builder()
+ .item(existingItem)
+ .build();
+ when(mockClient.putItem(any(PutItemRequest.class))).thenThrow(exception);
+
+ // WHEN / THEN
+ assertThatThrownBy(() -> persistenceStore.putRecord(dataRecord, now))
.isInstanceOf(IdempotencyItemAlreadyExistsException.class)
- // DataRecord should be present due to returnValuesOnConditionCheckFailure("ALL_OLD")
- .matches(e -> ((IdempotencyItemAlreadyExistsException) e).getDataRecord().isPresent());
-
- // THEN: item was not updated, retrieve the initial one
- Map itemInDb = client
- .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
- assertThat(itemInDb).isNotNull();
- assertThat(itemInDb.get("status").s()).isEqualTo("COMPLETED");
- assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry));
- assertThat(itemInDb.get("data").s()).isEqualTo("Fake Data");
+ .matches(e -> ((IdempotencyItemAlreadyExistsException) e).getDataRecord().isPresent())
+ .satisfies(e -> {
+ IdempotencyItemAlreadyExistsException ex = (IdempotencyItemAlreadyExistsException) e;
+ DataRecord existingRecord = ex.getDataRecord().get();
+ assertThat(existingRecord.getIdempotencyKey()).isEqualTo("key");
+ assertThat(existingRecord.getStatus()).isEqualTo(DataRecord.Status.COMPLETED);
+ assertThat(existingRecord.getResponseData()).isEqualTo("Existing Data");
+ });
}
@Test
- void putRecord_shouldBlockUpdate_IfRecordAlreadyExistAndProgressNotExpiredAfterLambdaTimedOut() {
- key = Collections.singletonMap("id", AttributeValue.builder().s("key").build());
-
- // GIVEN: Insert a fake item with same id
- Map item = new HashMap<>(key);
+ void putRecord_shouldThrowWithoutDataRecord_whenConditionalCheckHasNoItem() {
+ // GIVEN
Instant now = Instant.now();
- long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); // not expired
- long progressExpiry = now.plus(30, ChronoUnit.SECONDS).toEpochMilli(); // not expired
- item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build());
- item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build());
- item.put("data", AttributeValue.builder().s("Fake Data").build());
- item.put("in_progress_expiration", AttributeValue.builder().n(String.valueOf(progressExpiry)).build());
- client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build());
-
- // WHEN: call putRecord
- long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond();
- DataRecord recordToInsert = new DataRecord("key",
- DataRecord.Status.INPROGRESS,
- expiry2,
- "Fake Data 2",
- null);
- assertThatThrownBy(() -> dynamoDBPersistenceStore.putRecord(recordToInsert, now))
+ long expiry = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond();
+ DataRecord dataRecord = new DataRecord("key", DataRecord.Status.INPROGRESS, expiry, null, null);
+
+ ConditionalCheckFailedException exception = ConditionalCheckFailedException.builder().build();
+ when(mockClient.putItem(any(PutItemRequest.class))).thenThrow(exception);
+
+ // WHEN / THEN
+ assertThatThrownBy(() -> persistenceStore.putRecord(dataRecord, now))
.isInstanceOf(IdempotencyItemAlreadyExistsException.class)
- // DataRecord should be present due to returnValuesOnConditionCheckFailure("ALL_OLD")
- .matches(e -> ((IdempotencyItemAlreadyExistsException) e).getDataRecord().isPresent());
-
- // THEN: item was not updated, retrieve the initial one
- Map itemInDb = client
- .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
- assertThat(itemInDb).isNotNull();
- assertThat(itemInDb.get("status").s()).isEqualTo("INPROGRESS");
- assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry));
- assertThat(itemInDb.get("data").s()).isEqualTo("Fake Data");
+ .matches(e -> !((IdempotencyItemAlreadyExistsException) e).getDataRecord().isPresent());
}
@Test
- void getRecord_shouldReturnExistingRecord() throws IdempotencyItemNotFoundException {
- key = Collections.singletonMap("id", AttributeValue.builder().s("key").build());
-
- // GIVEN: Insert a fake item with same id
- Map item = new HashMap<>(key);
- Instant now = Instant.now();
- long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond();
+ void getRecord_shouldReturnDataRecord_whenItemExists() throws IdempotencyItemNotFoundException {
+ // GIVEN
+ long expiry = Instant.now().plus(3600, ChronoUnit.SECONDS).getEpochSecond();
+ Map item = new HashMap<>();
+ item.put("id", AttributeValue.builder().s("key").build());
+ item.put("status", AttributeValue.builder().s("COMPLETED").build());
item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build());
- item.put("status", AttributeValue.builder().s(DataRecord.Status.COMPLETED.toString()).build());
- item.put("data", AttributeValue.builder().s("Fake Data").build());
- client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build());
+ item.put("data", AttributeValue.builder().s("Response Data").build());
+
+ GetItemResponse response = GetItemResponse.builder()
+ .item(item)
+ .build();
+ when(mockClient.getItem(any(GetItemRequest.class))).thenReturn(response);
// WHEN
- DataRecord dr = dynamoDBPersistenceStore.getRecord("key");
+ DataRecord dataRecord = persistenceStore.getRecord("key");
// THEN
- assertThat(dr.getIdempotencyKey()).isEqualTo("key");
- assertThat(dr.getStatus()).isEqualTo(DataRecord.Status.COMPLETED);
- assertThat(dr.getResponseData()).isEqualTo("Fake Data");
- assertThat(dr.getExpiryTimestamp()).isEqualTo(expiry);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(GetItemRequest.class);
+ verify(mockClient).getItem(captor.capture());
+
+ GetItemRequest request = captor.getValue();
+ assertThat(request.tableName()).isEqualTo(TABLE_NAME);
+ assertThat(request.consistentRead()).isTrue();
+ assertThat(request.key()).containsEntry("id", AttributeValue.builder().s("key").build());
+
+ assertThat(dataRecord.getIdempotencyKey()).isEqualTo("key");
+ assertThat(dataRecord.getStatus()).isEqualTo(DataRecord.Status.COMPLETED);
+ assertThat(dataRecord.getExpiryTimestamp()).isEqualTo(expiry);
+ assertThat(dataRecord.getResponseData()).isEqualTo("Response Data");
}
@Test
- void getRecord_shouldThrowException_whenRecordIsAbsent() {
- assertThatThrownBy(() -> dynamoDBPersistenceStore.getRecord("key"))
+ void getRecord_shouldThrowException_whenItemDoesNotExist() {
+ // GIVEN
+ GetItemResponse response = GetItemResponse.builder().build();
+ when(mockClient.getItem(any(GetItemRequest.class))).thenReturn(response);
+
+ // WHEN / THEN
+ assertThatThrownBy(() -> persistenceStore.getRecord("key"))
.isInstanceOf(IdempotencyItemNotFoundException.class);
}
@Test
- void updateRecord_shouldUpdateRecord() {
- // GIVEN: Insert a fake item with same id
- key = Collections.singletonMap("id", AttributeValue.builder().s("key").build());
- Map item = new HashMap<>(key);
- Instant now = Instant.now();
- long expiry = now.plus(360, ChronoUnit.SECONDS).getEpochSecond();
- item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build());
- item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build());
- client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build());
- // enable payload validation
- dynamoDBPersistenceStore.configure(IdempotencyConfig.builder().withPayloadValidationJMESPath("path").build(),
- null);
+ void updateRecord_shouldSendCorrectUpdateItemRequest() {
+ // GIVEN
+ long expiry = Instant.now().plus(3600, ChronoUnit.SECONDS).getEpochSecond();
+ DataRecord dataRecord = new DataRecord("key", DataRecord.Status.COMPLETED, expiry, "Response", null);
// WHEN
- expiry = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond();
- DataRecord dr = new DataRecord("key", DataRecord.Status.COMPLETED, expiry, "Fake result", "hash");
- dynamoDBPersistenceStore.updateRecord(dr);
+ persistenceStore.updateRecord(dataRecord);
// THEN
- Map itemInDb = client
- .getItem(GetItemRequest.builder().tableName(TABLE_NAME).key(key).build()).item();
- assertThat(itemInDb.get("status").s()).isEqualTo("COMPLETED");
- assertThat(itemInDb.get("expiration").n()).isEqualTo(String.valueOf(expiry));
- assertThat(itemInDb.get("data").s()).isEqualTo("Fake result");
- assertThat(itemInDb.get("validation").s()).isEqualTo("hash");
+ ArgumentCaptor captor = ArgumentCaptor.forClass(UpdateItemRequest.class);
+ verify(mockClient).updateItem(captor.capture());
+
+ UpdateItemRequest request = captor.getValue();
+ assertThat(request.tableName()).isEqualTo(TABLE_NAME);
+ assertThat(request.key()).containsEntry("id", AttributeValue.builder().s("key").build());
+ assertThat(request.updateExpression()).contains("SET #response_data = :response_data");
+ assertThat(request.updateExpression()).contains("#expiry = :expiry");
+ assertThat(request.updateExpression()).contains("#status = :status");
+ assertThat(request.expressionAttributeValues().get(":response_data").s()).isEqualTo("Response");
+ assertThat(request.expressionAttributeValues().get(":status").s()).isEqualTo("COMPLETED");
}
@Test
- void deleteRecord_shouldDeleteRecord() {
- // GIVEN: Insert a fake item with same id
- key = Collections.singletonMap("id", AttributeValue.builder().s("key").build());
- Map item = new HashMap<>(key);
+ void updateRecord_shouldIncludeValidation_whenPayloadValidationEnabled() {
+ // GIVEN
+ persistenceStore.configure(IdempotencyConfig.builder().withPayloadValidationJMESPath("body").build(), null);
+ long expiry = Instant.now().plus(3600, ChronoUnit.SECONDS).getEpochSecond();
+ DataRecord dataRecord = new DataRecord("key", DataRecord.Status.COMPLETED, expiry, "Response", "hash123");
+
+ // WHEN
+ persistenceStore.updateRecord(dataRecord);
+
+ // THEN
+ ArgumentCaptor captor = ArgumentCaptor.forClass(UpdateItemRequest.class);
+ verify(mockClient).updateItem(captor.capture());
+
+ UpdateItemRequest request = captor.getValue();
+ assertThat(request.updateExpression()).contains("#validation_key = :validation_key");
+ assertThat(request.expressionAttributeValues().get(":validation_key").s()).isEqualTo("hash123");
+ }
+
+ @Test
+ void deleteRecord_shouldSendCorrectDeleteItemRequest() {
+ // WHEN
+ persistenceStore.deleteRecord("key");
+
+ // THEN
+ ArgumentCaptor captor = ArgumentCaptor.forClass(DeleteItemRequest.class);
+ verify(mockClient).deleteItem(captor.capture());
+
+ DeleteItemRequest request = captor.getValue();
+ assertThat(request.tableName()).isEqualTo(TABLE_NAME);
+ assertThat(request.key()).containsEntry("id", AttributeValue.builder().s("key").build());
+ }
+
+ @Test
+ void customAttributeNames_shouldUseCorrectAttributes() throws IdempotencyItemAlreadyExistsException {
+ // GIVEN
+ DynamoDBPersistenceStore customStore = DynamoDBPersistenceStore.builder()
+ .withTableName("custom_table")
+ .withDynamoDbClient(mockClient)
+ .withKeyAttr("pk")
+ .withSortKeyAttr("sk")
+ .withStaticPkValue("IDEMPOTENCY")
+ .withExpiryAttr("ttl")
+ .withStatusAttr("state")
+ .withDataAttr("result")
+ .withValidationAttr("hash")
+ .build();
+
Instant now = Instant.now();
- long expiry = now.plus(360, ChronoUnit.SECONDS).getEpochSecond();
- item.put("expiration", AttributeValue.builder().n(String.valueOf(expiry)).build());
- item.put("status", AttributeValue.builder().s(DataRecord.Status.INPROGRESS.toString()).build());
- client.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(item).build());
- assertThat(client.scan(ScanRequest.builder().tableName(TABLE_NAME).build()).count()).isEqualTo(1);
+ long expiry = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond();
+ DataRecord dataRecord = new DataRecord("mykey", DataRecord.Status.INPROGRESS, expiry, null, null);
// WHEN
- dynamoDBPersistenceStore.deleteRecord("key");
+ customStore.putRecord(dataRecord, now);
// THEN
- assertThat(client.scan(ScanRequest.builder().tableName(TABLE_NAME).build()).count()).isZero();
+ ArgumentCaptor captor = ArgumentCaptor.forClass(PutItemRequest.class);
+ verify(mockClient).putItem(captor.capture());
+
+ PutItemRequest request = captor.getValue();
+ assertThat(request.tableName()).isEqualTo("custom_table");
+ assertThat(request.item()).containsEntry("pk", AttributeValue.builder().s("IDEMPOTENCY").build());
+ assertThat(request.item()).containsEntry("sk", AttributeValue.builder().s("mykey").build());
+ assertThat(request.item().get("state").s()).isEqualTo("INPROGRESS");
+ assertThat(request.item().get("ttl").n()).isEqualTo(String.valueOf(expiry));
}
@Test
- void endToEndWithCustomAttrNamesAndSortKey() throws IdempotencyItemNotFoundException {
- try {
- client.createTable(CreateTableRequest.builder()
- .tableName(TABLE_NAME_CUSTOM)
- .keySchema(
- KeySchemaElement.builder().keyType(KeyType.HASH).attributeName("key").build(),
- KeySchemaElement.builder().keyType(KeyType.RANGE).attributeName("sortkey").build())
- .attributeDefinitions(
- AttributeDefinition.builder().attributeName("key").attributeType(ScalarAttributeType.S)
- .build(),
- AttributeDefinition.builder().attributeName("sortkey").attributeType(ScalarAttributeType.S)
- .build())
- .billingMode(BillingMode.PAY_PER_REQUEST)
- .build());
-
- DynamoDBPersistenceStore persistenceStore = DynamoDBPersistenceStore.builder()
- .withTableName(TABLE_NAME_CUSTOM)
- .withDynamoDbClient(client)
- .withDataAttr("result")
- .withExpiryAttr("expiry")
- .withKeyAttr("key")
- .withSortKeyAttr("sortkey")
- .withStaticPkValue("pk")
- .withStatusAttr("state")
- .withValidationAttr("valid")
- .build();
-
- Instant now = Instant.now();
- DataRecord dr = new DataRecord(
- "mykey",
- DataRecord.Status.INPROGRESS,
- now.plus(400, ChronoUnit.SECONDS).getEpochSecond(),
- null,
- null);
- // PUT
- persistenceStore.putRecord(dr, now);
-
- Map customKey = new HashMap<>();
- customKey.put("key", AttributeValue.builder().s("pk").build());
- customKey.put("sortkey", AttributeValue.builder().s("mykey").build());
-
- Map itemInDb = client
- .getItem(GetItemRequest.builder().tableName(TABLE_NAME_CUSTOM).key(customKey).build()).item();
-
- // GET
- DataRecord recordInDb = persistenceStore.getRecord("mykey");
-
- assertThat(itemInDb).isNotNull();
- assertThat(itemInDb.get("key").s()).isEqualTo("pk");
- assertThat(itemInDb.get("sortkey").s()).isEqualTo(recordInDb.getIdempotencyKey());
- assertThat(itemInDb.get("state").s()).isEqualTo(recordInDb.getStatus().toString());
- assertThat(itemInDb.get("expiry").n()).isEqualTo(String.valueOf(recordInDb.getExpiryTimestamp()));
-
- // UPDATE
- DataRecord updatedRecord = new DataRecord(
- "mykey",
- DataRecord.Status.COMPLETED,
- now.plus(500, ChronoUnit.SECONDS).getEpochSecond(),
- "response",
- null);
- persistenceStore.updateRecord(updatedRecord);
- recordInDb = persistenceStore.getRecord("mykey");
- assertThat(recordInDb).isEqualTo(updatedRecord);
-
- // DELETE
- persistenceStore.deleteRecord("mykey");
- assertThat(client.scan(ScanRequest.builder().tableName(TABLE_NAME_CUSTOM).build()).count()).isEqualTo(0);
-
- } finally {
- try {
- client.deleteTable(DeleteTableRequest.builder().tableName(TABLE_NAME_CUSTOM).build());
- } catch (Exception e) {
- // OK
- }
- }
+ void customAttributeNames_shouldUseCorrectKey_forGet() throws IdempotencyItemNotFoundException {
+ // GIVEN
+ DynamoDBPersistenceStore customStore = DynamoDBPersistenceStore.builder()
+ .withTableName("custom_table")
+ .withDynamoDbClient(mockClient)
+ .withKeyAttr("pk")
+ .withSortKeyAttr("sk")
+ .withStaticPkValue("IDEMPOTENCY")
+ .withExpiryAttr("ttl")
+ .withStatusAttr("state")
+ .build();
+
+ long expiry = Instant.now().plus(3600, ChronoUnit.SECONDS).getEpochSecond();
+ Map item = new HashMap<>();
+ item.put("pk", AttributeValue.builder().s("IDEMPOTENCY").build());
+ item.put("sk", AttributeValue.builder().s("mykey").build());
+ item.put("state", AttributeValue.builder().s("COMPLETED").build());
+ item.put("ttl", AttributeValue.builder().n(String.valueOf(expiry)).build());
+
+ GetItemResponse response = GetItemResponse.builder().item(item).build();
+ when(mockClient.getItem(any(GetItemRequest.class))).thenReturn(response);
+
+ // WHEN
+ DataRecord dataRecord = customStore.getRecord("mykey");
+
+ // THEN
+ ArgumentCaptor captor = ArgumentCaptor.forClass(GetItemRequest.class);
+ verify(mockClient).getItem(captor.capture());
+
+ GetItemRequest request = captor.getValue();
+ assertThat(request.key()).containsEntry("pk", AttributeValue.builder().s("IDEMPOTENCY").build());
+ assertThat(request.key()).containsEntry("sk", AttributeValue.builder().s("mykey").build());
+
+ assertThat(dataRecord.getIdempotencyKey()).isEqualTo("mykey");
+ assertThat(dataRecord.getStatus()).isEqualTo(DataRecord.Status.COMPLETED);
}
@Test
@SetEnvironmentVariable(key = Constants.IDEMPOTENCY_DISABLED_ENV, value = "true")
void idempotencyDisabled_noClientShouldBeCreated() {
- DynamoDBPersistenceStore store = DynamoDBPersistenceStore.builder().withTableName(TABLE_NAME).build();
- assertThatThrownBy(() -> store.getRecord("fake"))
- .isInstanceOf(NullPointerException.class);
- }
-
- @BeforeEach
- void setup() {
- dynamoDBPersistenceStore = DynamoDBPersistenceStore.builder()
+ // GIVEN / WHEN
+ DynamoDBPersistenceStore store = DynamoDBPersistenceStore.builder()
.withTableName(TABLE_NAME)
- .withDynamoDbClient(client)
.build();
- }
- @AfterEach
- void emptyDB() {
- // Clear all items from the table
- client.scan(ScanRequest.builder().tableName(TABLE_NAME).build())
- .items()
- .forEach(item -> {
- Map itemKey = Collections.singletonMap("id", item.get("id"));
- client.deleteItem(DeleteItemRequest.builder().tableName(TABLE_NAME).key(itemKey).build());
- });
- key = null;
+ // THEN
+ assertThatThrownBy(() -> store.getRecord("fake"))
+ .isInstanceOf(NullPointerException.class);
}
}
diff --git a/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/IdempotencyTest.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/IdempotencyTest.java
deleted file mode 100644
index e85614580..000000000
--- a/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/IdempotencyTest.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 2023 Amazon.com, Inc. or its affiliates.
- * Licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package software.amazon.lambda.powertools.idempotency.persistence.dynamodb;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import org.junit.jupiter.api.Test;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
-import com.amazonaws.services.lambda.runtime.tests.EventLoader;
-
-import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
-import software.amazon.lambda.powertools.common.stubs.TestLambdaContext;
-import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.handlers.IdempotencyFunction;
-
-class IdempotencyTest extends DynamoDBConfig {
-
- private Context context = new TestLambdaContext();
-
- @Test
- void endToEndTest() {
- IdempotencyFunction function = new IdempotencyFunction(client);
-
- APIGatewayProxyResponseEvent response = function
- .handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context);
- assertThat(function.handlerExecuted).isTrue();
-
- function.handlerExecuted = false;
-
- APIGatewayProxyResponseEvent response2 = function
- .handleRequest(EventLoader.loadApiGatewayRestEvent("apigw_event2.json"), context);
- assertThat(function.handlerExecuted).isFalse();
-
- assertThat(response).isEqualTo(response2);
- assertThat(response2.getBody()).contains("hello world");
-
- assertThat(client.scan(ScanRequest.builder().tableName(TABLE_NAME).build()).count()).isEqualTo(1);
- }
-}
diff --git a/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/handlers/IdempotencyFunction.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/handlers/IdempotencyFunction.java
deleted file mode 100644
index d816af801..000000000
--- a/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/handlers/IdempotencyFunction.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright 2023 Amazon.com, Inc. or its affiliates.
- * Licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package software.amazon.lambda.powertools.idempotency.persistence.dynamodb.handlers;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.amazonaws.services.lambda.runtime.RequestHandler;
-import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
-import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
-
-import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
-import software.amazon.lambda.powertools.idempotency.Idempotency;
-import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
-import software.amazon.lambda.powertools.idempotency.Idempotent;
-import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.DynamoDBPersistenceStore;
-
-public class IdempotencyFunction implements RequestHandler {
- public boolean handlerExecuted = false;
-
- public IdempotencyFunction(DynamoDbClient client) {
- // we need to initialize idempotency configuration before the handleRequest method is called
- Idempotency.config().withConfig(
- IdempotencyConfig.builder()
- .withEventKeyJMESPath("powertools_json(body).address")
- .build())
- .withPersistenceStore(
- DynamoDBPersistenceStore.builder()
- .withTableName("idempotency_table")
- .withDynamoDbClient(client)
- .build())
- .configure();
- }
-
- @Idempotent
- public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
- handlerExecuted = true;
- Map headers = new HashMap<>();
-
- headers.put("Content-Type", "application/json");
- headers.put("Access-Control-Allow-Origin", "*");
- headers.put("Access-Control-Allow-Methods", "GET, OPTIONS");
- headers.put("Access-Control-Allow-Headers", "*");
-
- APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
- .withHeaders(headers);
-
- return response
- .withStatusCode(200)
- .withBody("{ \"message\": \"hello world\"}");
-
- }
-}