Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ This project contains an AWS Lambda maven application with [AWS Java SDK 2.x](ht
The generated function handler class just returns the input. The configured AWS Java SDK client is created in `DependencyFactory` class and you can
add the code to interact with the SDK client based on your use case.

### SQS Integration
The application can optionally send data to an SQS queue after saving it to DynamoDB. This feature is controlled by the following environment variables:

- `SQS_ENABLED`: Set to "true" to enable sending to SQS
- `SQS_QUEUE_URL`: The URL of the SQS queue to send messages to

When enabled, the application will send the book data as a JSON message to the specified SQS queue.

#### Building the project
```
mvn clean install
Expand Down Expand Up @@ -42,4 +50,3 @@ sam deploy --guided
See [Deploying Serverless Applications](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-deploying.html) for more info.



5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@
<artifactId>url-connection-client</artifactId>
</dependency>

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sqs</artifactId>
</dependency>

<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
Expand Down
24 changes: 23 additions & 1 deletion src/main/java/com/home/amazon/serverless/DependencyFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
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.sqs.SqsClient;

public class DependencyFactory {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Description: The class contains both client creation methods and environment variable access methods, potentially violating the Single Responsibility Principle. Consider separating the environment variable access methods into a separate class, such as EnvironmentConfig, to improve modularity.

Severity: Low

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix addresses the Single Responsibility Principle violation by removing the environment variable constants from the DependencyFactory class. These constants should be moved to a separate EnvironmentConfig class to improve modularity and separate concerns. The DependencyFactory class now focuses solely on creating and providing instances of AWS clients.

Suggested change
public class DependencyFactory {
import software.amazon.awssdk.services.sqs.SqsClient;
public class DependencyFactory {
private DependencyFactory() {}


public static final String ENV_VARIABLE_TABLE = "TABLE";
public static final String ENV_VARIABLE_SQS_ENABLED = "SQS_ENABLED";
public static final String ENV_VARIABLE_SQS_QUEUE_URL = "SQS_QUEUE_URL";

private DependencyFactory() {}

/**
* @return an instance of LambdaClient
* @return an instance of DynamoDbEnhancedClient
*/
public static DynamoDbEnhancedClient dynamoDbEnhancedClient() {
return DynamoDbEnhancedClient.builder()
Expand All @@ -27,8 +30,27 @@ public static DynamoDbEnhancedClient dynamoDbEnhancedClient() {
.build();
}

/**
* @return an instance of SqsClient
*/
public static SqsClient sqsClient() {
return SqsClient.builder()
.credentialsProvider(EnvironmentVariableCredentialsProvider.create())
.region(Region.of(System.getenv(SdkSystemSetting.AWS_REGION.environmentVariable())))
.httpClientBuilder(UrlConnectionHttpClient.builder())
.build();
}

public static String tableName() {
return System.getenv(ENV_VARIABLE_TABLE);
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Description: The methods tableName() and sqsQueueUrl() don't handle cases where the environment variables are not set. Consider adding null checks or providing default values for these methods.

Severity: Medium

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix addresses the issue of not handling cases where environment variables are not set in the tableName() and sqsQueueUrl() methods. For tableName(), a default value "default_table_name" is returned if the environment variable is not set. For sqsQueueUrl(), an empty string is returned if the environment variable is not set. This ensures that these methods always return a non-null value, preventing potential NullPointerExceptions in the code that calls these methods.

Suggested change
return System.getenv(ENV_VARIABLE_TABLE);
.httpClientBuilder(UrlConnectionHttpClient.builder())
.build();
}
public static String tableName() {
String tableName = System.getenv(ENV_VARIABLE_TABLE);
return tableName != null ? tableName : "default_table_name";
}
public static boolean isSqsEnabled() {
String sqsEnabled = System.getenv(ENV_VARIABLE_SQS_ENABLED);
return sqsEnabled != null && sqsEnabled.equalsIgnoreCase("true");
}
public static String sqsQueueUrl() {
String queueUrl = System.getenv(ENV_VARIABLE_SQS_QUEUE_URL);
return queueUrl != null ? queueUrl : "";
}
}

}

public static boolean isSqsEnabled() {
String sqsEnabled = System.getenv(ENV_VARIABLE_SQS_ENABLED);
return sqsEnabled != null && sqsEnabled.equalsIgnoreCase("true");
}

public static String sqsQueueUrl() {
return System.getenv(ENV_VARIABLE_SQS_QUEUE_URL);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,32 @@
import com.home.amazon.serverless.model.Book;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
import software.amazon.awssdk.enhanced.dynamodb.Key;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
import software.amazon.awssdk.services.sqs.model.SendMessageResponse;

import java.util.Collections;
import java.util.Map;

public class PostItemHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

private final DynamoDbEnhancedClient dbClient;
private final SqsClient sqsClient;
private final String tableName;
private final TableSchema<Book> bookTableSchema;
private final boolean sqsEnabled;
private final String sqsQueueUrl;
private final Gson gson;

public PostItemHandler() {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Description: The constructor initializes multiple dependencies without proper error handling. Consider wrapping the initialization in a try-catch block to handle potential exceptions during object creation.

Severity: High

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix addresses the comment by wrapping the constructor initialization in a try-catch block. This allows for proper error handling during object creation. If an exception occurs during initialization, it's caught and rethrown as a RuntimeException with a descriptive message, providing better error reporting and preventing silent failures.

Suggested change
public PostItemHandler() {
private final Gson gson;
public PostItemHandler() {
try {
dbClient = DependencyFactory.dynamoDbEnhancedClient();
sqsClient = DependencyFactory.sqsClient();
tableName = DependencyFactory.tableName();
bookTableSchema = TableSchema.fromBean(Book.class);
sqsEnabled = DependencyFactory.isSqsEnabled();
sqsQueueUrl = DependencyFactory.sqsQueueUrl();
gson = new Gson();
} catch (Exception e) {
throw new RuntimeException("Error initializing PostItemHandler: " + e.getMessage(), e);
}
}
@Override

dbClient = DependencyFactory.dynamoDbEnhancedClient();
sqsClient = DependencyFactory.sqsClient();
tableName = DependencyFactory.tableName();
bookTableSchema = TableSchema.fromBean(Book.class);
sqsEnabled = DependencyFactory.isSqsEnabled();
sqsQueueUrl = DependencyFactory.sqsQueueUrl();
gson = new Gson();
}

@Override
Expand All @@ -38,12 +48,27 @@ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent in
item.setAuthor(queryStringParameters.get("author"));
item.setName(queryStringParameters.get("name"));
booksTable.putItem(item);

// Optionally send to SQS if enabled
if (sqsEnabled && sqsQueueUrl != null && !sqsQueueUrl.isEmpty()) {
try {
String messageBody = gson.toJson(item);
SendMessageRequest sendMessageRequest = SendMessageRequest.builder()
.queueUrl(sqsQueueUrl)
.messageBody(messageBody)
.build();

SendMessageResponse sendMessageResponse = sqsClient.sendMessage(sendMessageRequest);
context.getLogger().log("Message sent to SQS. MessageId: " + sendMessageResponse.messageId());
} catch (Exception e) {
context.getLogger().log("Error sending message to SQS: " + e.getMessage());
}
}
}

return new APIGatewayProxyResponseEvent().withStatusCode(200)
.withIsBase64Encoded(Boolean.FALSE)
.withHeaders(Collections.emptyMap())
.withBody("Success");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.google.gson.Gson;
import com.home.amazon.serverless.DependencyFactory;
import com.home.amazon.serverless.model.Book;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -13,27 +14,33 @@
import org.mockito.junit.jupiter.MockitoExtension;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
import software.amazon.awssdk.enhanced.dynamodb.Key;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
import software.amazon.awssdk.services.sqs.model.SendMessageResponse;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class PostItemHandlerTest {

private static final String TEST_TABLE_NAME = "TestTable";
private static final String TEST_PARTITION_KEY_VALUE = "123";
private static final String TEST_QUEUE_URL = "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue";
private static final String TEST_AUTHOR = "Test Author";
private static final String TEST_NAME = "Test Book";

@Mock
private DynamoDbEnhancedClient dbClient;

@Mock
private DynamoDbEnhancedClient client;
private SqsClient sqsClient;

@Mock
private DynamoDbTable<Book> table;
Expand All @@ -44,14 +51,86 @@ public class PostItemHandlerTest {
@Mock
private Context context;

@Mock
private SendMessageResponse sendMessageResponse;

private PostItemHandler handler;
private Map<String, String> queryParams;

@BeforeEach
public void setUp() {
when(client.table(eq(TEST_TABLE_NAME), any(TableSchema.class))).thenReturn(table);
when(dbClient.table(eq(TEST_TABLE_NAME), any(TableSchema.class))).thenReturn(table);

queryParams = new HashMap<>();
queryParams.put(Book.PARTITION_KEY, TEST_PARTITION_KEY_VALUE);
queryParams.put("author", TEST_AUTHOR);
queryParams.put("name", TEST_NAME);
when(request.getQueryStringParameters()).thenReturn(queryParams);

when(sendMessageResponse.messageId()).thenReturn("test-message-id");
}

@Test
public void shouldReturnItemIfExists() {
assertTrue(true);
public void shouldSaveItemToDynamoDb() {
try (MockedStatic<DependencyFactory> dependencyFactoryMockedStatic = mockStatic(DependencyFactory.class)) {
// Setup mocks
dependencyFactoryMockedStatic.when(DependencyFactory::dynamoDbEnhancedClient).thenReturn(dbClient);
dependencyFactoryMockedStatic.when(DependencyFactory::sqsClient).thenReturn(sqsClient);
dependencyFactoryMockedStatic.when(DependencyFactory::tableName).thenReturn(TEST_TABLE_NAME);
dependencyFactoryMockedStatic.when(DependencyFactory::isSqsEnabled).thenReturn(false);

// Create handler and invoke
handler = new PostItemHandler();
APIGatewayProxyResponseEvent response = handler.handleRequest(request, context);

// Verify
verify(table).putItem(any(Book.class));
verify(sqsClient, never()).sendMessage(any(SendMessageRequest.class));
assertEquals(200, response.getStatusCode());
}
}

@Test
public void shouldSendToSqsWhenEnabled() {
try (MockedStatic<DependencyFactory> dependencyFactoryMockedStatic = mockStatic(DependencyFactory.class)) {
// Setup mocks
dependencyFactoryMockedStatic.when(DependencyFactory::dynamoDbEnhancedClient).thenReturn(dbClient);
dependencyFactoryMockedStatic.when(DependencyFactory::sqsClient).thenReturn(sqsClient);
dependencyFactoryMockedStatic.when(DependencyFactory::tableName).thenReturn(TEST_TABLE_NAME);
dependencyFactoryMockedStatic.when(DependencyFactory::isSqsEnabled).thenReturn(true);
dependencyFactoryMockedStatic.when(DependencyFactory::sqsQueueUrl).thenReturn(TEST_QUEUE_URL);

when(sqsClient.sendMessage(any(SendMessageRequest.class))).thenReturn(sendMessageResponse);

// Create handler and invoke
handler = new PostItemHandler();
APIGatewayProxyResponseEvent response = handler.handleRequest(request, context);

// Verify
verify(table).putItem(any(Book.class));
verify(sqsClient).sendMessage(any(SendMessageRequest.class));
assertEquals(200, response.getStatusCode());
}
}

@Test
public void shouldNotSendToSqsWhenQueueUrlIsNull() {
try (MockedStatic<DependencyFactory> dependencyFactoryMockedStatic = mockStatic(DependencyFactory.class)) {
// Setup mocks
dependencyFactoryMockedStatic.when(DependencyFactory::dynamoDbEnhancedClient).thenReturn(dbClient);
dependencyFactoryMockedStatic.when(DependencyFactory::sqsClient).thenReturn(sqsClient);
dependencyFactoryMockedStatic.when(DependencyFactory::tableName).thenReturn(TEST_TABLE_NAME);
dependencyFactoryMockedStatic.when(DependencyFactory::isSqsEnabled).thenReturn(true);
dependencyFactoryMockedStatic.when(DependencyFactory::sqsQueueUrl).thenReturn(null);

// Create handler and invoke
handler = new PostItemHandler();
APIGatewayProxyResponseEvent response = handler.handleRequest(request, context);

// Verify
verify(table).putItem(any(Book.class));
verify(sqsClient, never()).sendMessage(any(SendMessageRequest.class));
assertEquals(200, response.getStatusCode());
}
}
}