diff options
author | Thorsten Ortlepp <post@ortlepp.eu> | 2024-04-26 00:30:46 +0200 |
---|---|---|
committer | Thorsten Ortlepp <post@ortlepp.eu> | 2024-04-26 00:30:46 +0200 |
commit | e03b55be17261ed13ddf421bcf4a804a083a7614 (patch) | |
tree | 8512120756c494efe53c64f33e61a20316c17186 | |
parent | b6bdf180c777566bbe908303774e53c7d4e099c4 (diff) | |
download | notification-sender-e03b55be17261ed13ddf421bcf4a804a083a7614.zip |
added implementation
14 files changed, 538 insertions, 10 deletions
@@ -6,7 +6,7 @@ <groupId>eu.ortlepp</groupId> <artifactId>notification-sender</artifactId> - <version>1.0-SNAPSHOT</version> + <version>1.0</version> <properties> <maven.compiler.source>21</maven.compiler.source> @@ -14,6 +14,18 @@ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> + <dependencyManagement> + <dependencies> + <dependency> + <groupId>software.amazon.awssdk</groupId> + <artifactId>bom</artifactId> + <version>2.25.35</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + <dependencies> <dependency> <groupId>com.amazonaws</groupId> @@ -22,6 +34,17 @@ </dependency> <dependency> + <groupId>software.amazon.awssdk</groupId> + <artifactId>sns</artifactId> + </dependency> + + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <version>2.17.0</version> + </dependency> + + <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.2</version> @@ -38,6 +61,20 @@ <version>3.5.2</version> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> + <filters> + <filter> + <artifact>*:*</artifact> + <excludes> + <exclude>META-INF/MANIFEST.MF</exclude> + <exclude>META-INF/*DEPENDENCIES*</exclude> + <exclude>META-INF/*LICENSE*</exclude> + <exclude>META-INF/*NOTICE*</exclude> + <exclude>META-INF/*.SF</exclude> + <exclude>META-INF/*.DSA</exclude> + <exclude>META-INF/*.RSA</exclude> + </excludes> + </filter> + </filters> </configuration> <executions> <execution> @@ -48,7 +85,38 @@ </execution> </executions> </plugin> + + <plugin> + <artifactId>maven-surefire-plugin</artifactId> + <version>3.2.5</version> + </plugin> + + <plugin> + <artifactId>maven-failsafe-plugin</artifactId> + <version>3.2.5</version> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-checkstyle-plugin</artifactId> + <version>3.3.1</version> + <configuration> + <configLocation>google_checks.xml</configLocation> + <consoleOutput>true</consoleOutput> + <failsOnError>true</failsOnError> + <linkXRef>false</linkXRef> + </configuration> + <executions> + <execution> + <id>validate</id> + <phase>validate</phase> + <goals> + <goal>check</goal> + </goals> + </execution> + </executions> + </plugin> </plugins> </build> -</project>
\ No newline at end of file +</project> diff --git a/src/main/java/eu/ortlepp/notificationsender/NotificationHandler.java b/src/main/java/eu/ortlepp/notificationsender/NotificationHandler.java index 43e1aa9..17b4a46 100644 --- a/src/main/java/eu/ortlepp/notificationsender/NotificationHandler.java +++ b/src/main/java/eu/ortlepp/notificationsender/NotificationHandler.java @@ -1,12 +1,97 @@ package eu.ortlepp.notificationsender; import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.ortlepp.notificationsender.model.Notifications; +import eu.ortlepp.notificationsender.model.Response; +import eu.ortlepp.notificationsender.model.Status; +import eu.ortlepp.notificationsender.service.NotificationSender; +import eu.ortlepp.notificationsender.service.SnsNotificationSender; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; -public class NotificationHandler implements RequestHandler<String, String> { +/** + * AWS Lambda implementation. + */ +public class NotificationHandler implements RequestHandler<Map<String, Object>, Response> { - public String handleRequest(String event, Context context) { - return event.toLowerCase(); + private LambdaLogger logger; + + private NotificationSender sender; + + + /** + * Standard constructor for AWS runtime. Uses default NotificationSender implementation. + */ + public NotificationHandler() { + this.sender = null; + } + + + /** + * Constructor with custom NotificationSender implementation. Mainly for testing purposes. + * + * @param sender A custom NotificationSender + */ + public NotificationHandler(final NotificationSender sender) { + this.sender = sender; + } + + + /** + * Constructor with custom NotificationSender and Context implementation. + * Mainly for testing purposes. + * + * @param sender A custom NotificationSender + * @param context A custom Context (to initialize the build-in logger) + */ + public NotificationHandler(final NotificationSender sender, final Context context) { + this.sender = sender; + this.logger = context.getLogger(); + } + + + /** + * The AWS Lambda handler; executes the Lambda functionality. + * + * @param event The incoming event which triggered the Lambda + * @param context The context of the Lambda execution + * @return The final status of the execution + */ + public Response handleRequest(final Map<String, Object> event, final Context context) { + logger = context.getLogger(); + if (sender == null) { + sender = new SnsNotificationSender(logger); + } + + List<String> notifications = extractNotifications(event.get("body")); + if (!notifications.isEmpty() && sender.sendNotifications(notifications)) { + return new Response(Status.SUCCESS); + } + return new Response(Status.FAILED); + } + + + /** + * Extract the notifications from the input JSON. + * + * @param body The input JSON + * @return The extracted messages; empty list if an error occurred while parsing the JSON + */ + protected List<String> extractNotifications(final Object body) { + try { + Notifications notifications = + new ObjectMapper().readValue(body.toString(), Notifications.class); + return Arrays.asList(notifications.messages()); + } catch (JsonProcessingException ex) { + logger.log("parsing input JSON failed : " + ex.getMessage()); + return new ArrayList<>(); + } } -}
\ No newline at end of file +} diff --git a/src/main/java/eu/ortlepp/notificationsender/model/Notifications.java b/src/main/java/eu/ortlepp/notificationsender/model/Notifications.java new file mode 100644 index 0000000..65f7620 --- /dev/null +++ b/src/main/java/eu/ortlepp/notificationsender/model/Notifications.java @@ -0,0 +1,8 @@ +package eu.ortlepp.notificationsender.model; + +/** + * The expected input data of the Lambda. + * + * @param messages The messages / notifications + */ +public record Notifications(String[] messages) {} diff --git a/src/main/java/eu/ortlepp/notificationsender/model/Response.java b/src/main/java/eu/ortlepp/notificationsender/model/Response.java new file mode 100644 index 0000000..d931bed --- /dev/null +++ b/src/main/java/eu/ortlepp/notificationsender/model/Response.java @@ -0,0 +1,8 @@ +package eu.ortlepp.notificationsender.model; + +/** + * The response of a Lambda execution. + * + * @param status The final execution status of the Lambda + */ +public record Response(Status status) {} diff --git a/src/main/java/eu/ortlepp/notificationsender/model/Status.java b/src/main/java/eu/ortlepp/notificationsender/model/Status.java new file mode 100644 index 0000000..caeb0cd --- /dev/null +++ b/src/main/java/eu/ortlepp/notificationsender/model/Status.java @@ -0,0 +1,9 @@ +package eu.ortlepp.notificationsender.model; + +/** + * The status of a Lambda execution, shown in the response. + */ +public enum Status { + SUCCESS, + FAILED +} diff --git a/src/main/java/eu/ortlepp/notificationsender/service/NotificationSender.java b/src/main/java/eu/ortlepp/notificationsender/service/NotificationSender.java new file mode 100644 index 0000000..8c39c72 --- /dev/null +++ b/src/main/java/eu/ortlepp/notificationsender/service/NotificationSender.java @@ -0,0 +1,41 @@ +package eu.ortlepp.notificationsender.service; + +import java.util.List; + +/** + * Interface for notification senders. + */ +public interface NotificationSender { + + /** + * Send a list of notifications. + * + * @param notifications The notifications to send + * @return The result of the sending; true = successful, false = error occurred + */ + boolean sendNotifications(final List<String> notifications); + + + /** + * Format notifications for sending. + * If only one notification is passed, it is returned as is. + * If multiple notifications are passed, a numbered list of notifications is returned. + * + * @param notifications The notifications to format + * @return The formatted notifications + */ + default String formatNotifications(final List<String> notifications) { + if (notifications.size() == 1) { + return notifications.getFirst() + "\n\n"; + } + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < notifications.size(); i++) { + builder.append(i + 1) + .append(". Notification:\n") + .append(notifications.get(i)) + .append("\n\n"); + } + return builder.toString(); + } + +} diff --git a/src/main/java/eu/ortlepp/notificationsender/service/SnsNotificationSender.java b/src/main/java/eu/ortlepp/notificationsender/service/SnsNotificationSender.java new file mode 100644 index 0000000..958365a --- /dev/null +++ b/src/main/java/eu/ortlepp/notificationsender/service/SnsNotificationSender.java @@ -0,0 +1,63 @@ +package eu.ortlepp.notificationsender.service; + +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import eu.ortlepp.notificationsender.util.Config; +import java.util.List; +import software.amazon.awssdk.http.HttpStatusCode; +import software.amazon.awssdk.services.sns.SnsClient; +import software.amazon.awssdk.services.sns.model.PublishRequest; +import software.amazon.awssdk.services.sns.model.PublishResponse; +import software.amazon.awssdk.services.sns.model.SnsException; + +/** + * A notification sender for Amazon SNS. + */ +public class SnsNotificationSender implements NotificationSender { + + private final LambdaLogger logger; + + + /** + * Constructor to initialize the sender. + * + * @param logger The logger of the AWS Lambda + */ + public SnsNotificationSender(final LambdaLogger logger) { + this.logger = logger; + } + + + /** + * Send a list of notifications to the configured SNS topic. + * All notifications are combined into a single message. + * + * @param notifications The notifications to send + * @return The result of the sending; true = successful, false = error occurred + */ + @Override + public boolean sendNotifications(final List<String> notifications) { + String notification = formatNotifications(notifications); + + try (SnsClient snsClient = SnsClient.builder().region(Config.REGION).build()) { + PublishRequest request = + PublishRequest.builder().message(notification).topicArn(Config.ARN).build(); + PublishResponse result = snsClient.publish(request); + int resultStatusCode = result.sdkHttpResponse().statusCode(); + + if (resultStatusCode == HttpStatusCode.OK) { + logger.log("notification sent successfully"); + return true; + } + + logger.log("sending notification failed with status " + resultStatusCode); + logger.log("failed message was " + notification); + return false; + + } catch (SnsException ex) { + logger.log("sending notification failed: " + ex.getMessage()); + logger.log("failed message was " + notification); + return false; + } + } + +} diff --git a/src/main/java/eu/ortlepp/notificationsender/util/Config.java b/src/main/java/eu/ortlepp/notificationsender/util/Config.java new file mode 100644 index 0000000..139ea24 --- /dev/null +++ b/src/main/java/eu/ortlepp/notificationsender/util/Config.java @@ -0,0 +1,40 @@ +package eu.ortlepp.notificationsender.util; + +import software.amazon.awssdk.regions.Region; + +/** + * Required configuration values read from environment variables. + */ +public final class Config { + + /** + * The region of the SNS topic. The Name of the environment variable is REGION. + * Value example: eu-central-1 + */ + public static final Region REGION; + + /** + * The ARN of the SNS topic. The Name of the environment variable is ARN. + * Value example: arn:aws:sns:eu-central-1:123456789:Topic-Name + */ + public static final String ARN; + + + static { + REGION = Region.of(getEnvVar("REGION")); + ARN = getEnvVar("ARN"); + } + + + private static String getEnvVar(final String key) { + String value = System.getenv(key); + if (value == null) { + throw new RuntimeException("environment variable not set: " + key); + } + return value; + } + + + private Config() {} + +} diff --git a/src/main/resources/.gitkeep b/src/main/resources/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/main/resources/.gitkeep diff --git a/src/test/java/eu/ortlepp/notificationsender/NotificationHandlerTest.java b/src/test/java/eu/ortlepp/notificationsender/NotificationHandlerTest.java index efe768f..9777356 100644 --- a/src/test/java/eu/ortlepp/notificationsender/NotificationHandlerTest.java +++ b/src/test/java/eu/ortlepp/notificationsender/NotificationHandlerTest.java @@ -1,16 +1,79 @@ package eu.ortlepp.notificationsender; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import com.amazonaws.services.lambda.runtime.Context; +import eu.ortlepp.notificationsender.fake.FakeContext; +import eu.ortlepp.notificationsender.fake.FakeNotificationSenderFail; +import eu.ortlepp.notificationsender.fake.FakeNotificationSenderSuccess; +import eu.ortlepp.notificationsender.model.Response; +import eu.ortlepp.notificationsender.model.Status; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; public class NotificationHandlerTest { + private static final Object INPUT_VALID = "{\"messages\":[\"first\",\"second\"]}"; + private static final Object INPUT_INVALID = "{\"messages\":\"some text\"}"; + + private final Context context = new FakeContext(); + + + @Test + @DisplayName("Test sending notification successful") + void testHandleRequestSuccess() { + var input = new HashMap<String, Object>(); + input.put("body", INPUT_VALID); + NotificationHandler handler = new NotificationHandler(new FakeNotificationSenderSuccess()); + + assertEquals(new Response(Status.SUCCESS), handler.handleRequest(input, context)); + } + + + @Test + @DisplayName("Test sending notification fails") + void testHandleRequestFailSending() { + var input = new HashMap<String, Object>(); + input.put("body", INPUT_VALID); + NotificationHandler handler = new NotificationHandler(new FakeNotificationSenderFail()); + + assertEquals(new Response(Status.FAILED), handler.handleRequest(input, context)); + } + + + @Test + @DisplayName("Test sending notification fails because of invalid input") + void testHandleRequestFailInput() { + var input = new HashMap<String, Object>(); + input.put("body", INPUT_INVALID); + NotificationHandler handler = new NotificationHandler(new FakeNotificationSenderSuccess()); + + assertEquals(new Response(Status.FAILED), handler.handleRequest(input, context)); + } + + @Test - void testHandleRequest() { - String value = "Test"; - NotificationHandler handler = new NotificationHandler(); - assertEquals(value.toLowerCase(), handler.handleRequest(value, null)); + @DisplayName("Test extracting notifications from JSON successful") + void testExtractNotifications() { + List<String> expected = List.of("first", "second"); + NotificationHandler handler = new NotificationHandler(new FakeNotificationSenderSuccess()); + + assertIterableEquals(expected, handler.extractNotifications(INPUT_VALID)); + } + + + @Test + @DisplayName("Test extraction notifications from invalid JSON") + void testExtractNotificationsException() { + var expected = new ArrayList<String>(); + NotificationHandler handler = + new NotificationHandler(new FakeNotificationSenderSuccess(), new FakeContext()); + + assertIterableEquals(expected, handler.extractNotifications(INPUT_INVALID)); } } diff --git a/src/test/java/eu/ortlepp/notificationsender/fake/FakeContext.java b/src/test/java/eu/ortlepp/notificationsender/fake/FakeContext.java new file mode 100644 index 0000000..78b6bef --- /dev/null +++ b/src/test/java/eu/ortlepp/notificationsender/fake/FakeContext.java @@ -0,0 +1,82 @@ +package eu.ortlepp.notificationsender.fake; + +import com.amazonaws.services.lambda.runtime.ClientContext; +import com.amazonaws.services.lambda.runtime.CognitoIdentity; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import java.util.Arrays; + +public class FakeContext implements Context { + + private final LambdaLogger lambdaLogger = new FakeLambdaLogger(); + + @Override + public String getAwsRequestId() { + return ""; + } + + @Override + public String getLogGroupName() { + return ""; + } + + @Override + public String getLogStreamName() { + return ""; + } + + @Override + public String getFunctionName() { + return ""; + } + + @Override + public String getFunctionVersion() { + return ""; + } + + @Override + public String getInvokedFunctionArn() { + return ""; + } + + @Override + public CognitoIdentity getIdentity() { + return null; + } + + @Override + public ClientContext getClientContext() { + return null; + } + + @Override + public int getRemainingTimeInMillis() { + return 0; + } + + @Override + public int getMemoryLimitInMB() { + return 0; + } + + @Override + public LambdaLogger getLogger() { + return lambdaLogger; + } + + + private static class FakeLambdaLogger implements LambdaLogger { + + @Override + public void log(final String string) { + System.out.println(string); + } + + @Override + public void log(final byte[] bytes) { + System.out.println(Arrays.toString(bytes)); + } + } + +} diff --git a/src/test/java/eu/ortlepp/notificationsender/fake/FakeNotificationSenderFail.java b/src/test/java/eu/ortlepp/notificationsender/fake/FakeNotificationSenderFail.java new file mode 100644 index 0000000..fde5874 --- /dev/null +++ b/src/test/java/eu/ortlepp/notificationsender/fake/FakeNotificationSenderFail.java @@ -0,0 +1,13 @@ +package eu.ortlepp.notificationsender.fake; + +import eu.ortlepp.notificationsender.service.NotificationSender; +import java.util.List; + +public class FakeNotificationSenderFail implements NotificationSender { + + @Override + public boolean sendNotifications(final List<String> notifications) { + return false; + } + +} diff --git a/src/test/java/eu/ortlepp/notificationsender/fake/FakeNotificationSenderSuccess.java b/src/test/java/eu/ortlepp/notificationsender/fake/FakeNotificationSenderSuccess.java new file mode 100644 index 0000000..d342ded --- /dev/null +++ b/src/test/java/eu/ortlepp/notificationsender/fake/FakeNotificationSenderSuccess.java @@ -0,0 +1,13 @@ +package eu.ortlepp.notificationsender.fake; + +import eu.ortlepp.notificationsender.service.NotificationSender; +import java.util.List; + +public class FakeNotificationSenderSuccess implements NotificationSender { + + @Override + public boolean sendNotifications(final List<String> notifications) { + return true; + } + +} diff --git a/src/test/java/eu/ortlepp/notificationsender/service/NotificationSenderTest.java b/src/test/java/eu/ortlepp/notificationsender/service/NotificationSenderTest.java new file mode 100644 index 0000000..b2a5c68 --- /dev/null +++ b/src/test/java/eu/ortlepp/notificationsender/service/NotificationSenderTest.java @@ -0,0 +1,35 @@ +package eu.ortlepp.notificationsender.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import eu.ortlepp.notificationsender.fake.FakeNotificationSenderSuccess; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class NotificationSenderTest { + + private final NotificationSender sender = new FakeNotificationSenderSuccess(); + + + @Test + @DisplayName("Test formatting with one notification") + void testFormatNotificationsSingle() { + var input = List.of("test notification"); + var expected = "test notification\n\n"; + + assertEquals(expected, sender.formatNotifications(input)); + } + + + @Test + @DisplayName("Test formatting with multiple notifications") + void testFormatNotificationsMultiple() { + var input = List.of("first notification", "second notification"); + var expected = + "1. Notification:\nfirst notification\n\n2. Notification:\nsecond notification\n\n"; + + assertEquals(expected, sender.formatNotifications(input)); + } + +} |