diff --git a/Backend/retry/docker-compose.yml b/Backend/retry/docker-compose.yml
index ea6fa4c..0130179 100644
--- a/Backend/retry/docker-compose.yml
+++ b/Backend/retry/docker-compose.yml
@@ -16,3 +16,14 @@ services:
ports:
- 1025:1025
- 8025:8025
+
+ mongo-express:
+ container_name: mongoexpress
+ image: mongo-express
+ links:
+ - mongodb
+ restart: always
+ ports:
+ - 8081:8081
+ environment:
+ ME_CONFIG_MONGODB_URL: mongodb://mongodb:27017/db
\ No newline at end of file
diff --git a/Backend/retry/pom.xml b/Backend/retry/pom.xml
index 0ccfb9d..ef058bc 100644
--- a/Backend/retry/pom.xml
+++ b/Backend/retry/pom.xml
@@ -45,6 +45,41 @@
+
+ org.springframework.boot
+ spring-boot-starter-validation
+ 3.0.4
+
+
+ org.springframework.data
+ spring-data-mongodb
+
+
+ org.springframework.integration
+ spring-integration-mongodb
+
+
+ org.mongodb
+ mongodb-driver-sync
+ 4.9.1
+ compile
+
+
+ io.springfox
+ springfox-swagger2
+ 2.7.0
+
+
+ io.springfox
+ springfox-swagger-ui
+ 2.7.0
+
+
+ org.mockito
+ mockito-core
+ test
+
+
diff --git a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/RetryApplication.java b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/RetryApplication.java
index ff42582..8fad27b 100644
--- a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/RetryApplication.java
+++ b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/RetryApplication.java
@@ -2,14 +2,29 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
@SpringBootApplication
@EnableAsync
+@EnableSwagger2 //http://localhost:8080/swagger-ui.html
public class RetryApplication {
public static void main(String[] args) {
SpringApplication.run(RetryApplication.class, args);
}
+ @Bean
+ public Docket retryApi() {
+ return new Docket(DocumentationType.SWAGGER_2)
+ .select()
+ .apis(RequestHandlerSelectors.any())
+ .paths(PathSelectors.any())
+ .build();
+ }
}
diff --git a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/configuration/GlobalBeanConfiguration.java b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/configuration/GlobalBeanConfiguration.java
index d14297e..ea3bae6 100644
--- a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/configuration/GlobalBeanConfiguration.java
+++ b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/configuration/GlobalBeanConfiguration.java
@@ -1,16 +1,68 @@
package com.staffinghub.coding.challenges.retry.configuration;
+import com.staffinghub.coding.challenges.retry.core.entities.EmailNotificationMessage;
import com.staffinghub.coding.challenges.retry.core.inbound.NotificationHandler;
import com.staffinghub.coding.challenges.retry.core.logic.NotificationService;
-import com.staffinghub.coding.challenges.retry.core.outbound.NotificationSender;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.data.mongodb.MongoDbFactory;
+import org.springframework.integration.channel.QueueChannel;
+import org.springframework.integration.dsl.IntegrationFlow;
+import org.springframework.integration.dsl.IntegrationFlows;
+import org.springframework.integration.dsl.Pollers;
+import org.springframework.integration.mongodb.store.ConfigurableMongoDbMessageStore;
+import org.springframework.integration.mongodb.store.MongoDbChannelMessageStore;
+import org.springframework.integration.store.MessageGroupQueue;
+import org.springframework.messaging.PollableChannel;
+
+import java.time.LocalDateTime;
+import java.util.concurrent.Executors;
@Configuration
+@Slf4j
public class GlobalBeanConfiguration {
+ private static final String GROUP_ID = "email-group";
+ private static final String COLLECTION_NAME = "message-store";
+ @Bean
+ public NotificationHandler notificationHandler(PollableChannel channel) {
+ return new NotificationService(channel);
+ }
+
+ @Bean
+ public MongoDbChannelMessageStore mongoDbChannelMessageStore(MongoDbFactory MongoDbFactory) {
+ return new MongoDbChannelMessageStore(MongoDbFactory, COLLECTION_NAME);
+ }
+
+ @Bean
+ public ConfigurableMongoDbMessageStore configurableMongoDbMessageStore(MongoDbFactory MongoDbFactory) {
+ return new ConfigurableMongoDbMessageStore(MongoDbFactory, COLLECTION_NAME);
+ }
+
+ @Bean
+ public PollableChannel channel(MongoDbChannelMessageStore messageStore) {
+ MessageGroupQueue messageGroupQueue = new MessageGroupQueue(messageStore, GROUP_ID);
+ return new QueueChannel(messageGroupQueue);
+ }
@Bean
- public NotificationHandler notificationHandler(NotificationSender notificationSender) {
- return new NotificationService(notificationSender);
+ public IntegrationFlow integrationFlow(PollableChannel channel, ApplicationEventPublisher events) {
+ return IntegrationFlows
+ .from(channel)
+ .filter(
+ EmailNotificationMessage.class,
+ p -> p.getDueTimestamp().isBefore(LocalDateTime.now()),
+ e -> e.poller(Pollers
+ .fixedRate(7000, 5000)
+ .maxMessagesPerPoll(1)
+ .taskExecutor(Executors.newSingleThreadExecutor())))
+ .handle(message -> {
+ EmailNotificationMessage emailNotificationMessage = (EmailNotificationMessage)message.getPayload();
+ events.publishEvent(emailNotificationMessage);
+ log.info("Message pulled: {}", message.getPayload());
+ }
+ )
+ .get();
}
}
diff --git a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/entities/EmailNotification.java b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/entities/EmailNotification.java
index 7405b98..47792b1 100644
--- a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/entities/EmailNotification.java
+++ b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/entities/EmailNotification.java
@@ -1,11 +1,18 @@
package com.staffinghub.coding.challenges.retry.core.entities;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
@Data
-public class EmailNotification {
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class EmailNotification implements Serializable {
@NotBlank
private String recipient;
diff --git a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/entities/EmailNotificationMessage.java b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/entities/EmailNotificationMessage.java
new file mode 100644
index 0000000..40726e2
--- /dev/null
+++ b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/entities/EmailNotificationMessage.java
@@ -0,0 +1,26 @@
+package com.staffinghub.coding.challenges.retry.core.entities;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * Message used for queueing incoming email notifications.
+ */
+@Data
+@Builder
+public class EmailNotificationMessage implements Serializable {
+
+ @Builder.Default
+ private LocalDateTime timestamp = LocalDateTime.now();
+
+ @Builder.Default
+ private LocalDateTime dueTimestamp = LocalDateTime.now();
+
+ @Builder.Default
+ private Integer retryAttempts = 0;
+
+ private EmailNotification emailNotification;
+}
diff --git a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/inbound/NotificationHandler.java b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/inbound/NotificationHandler.java
index f6aa6ab..85aa146 100644
--- a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/inbound/NotificationHandler.java
+++ b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/inbound/NotificationHandler.java
@@ -4,5 +4,10 @@
public interface NotificationHandler {
+ /**
+ * Submits an emailNotification {@link EmailNotification} onto the email notification queue to handle the submission of the given notification
+ * @param emailNotification {@link EmailNotification}
+ * @return {@link EmailNotification} The email notification that was submitted for processing
+ */
EmailNotification processEmailNotification(EmailNotification emailNotification);
}
diff --git a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/logic/NotificationService.java b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/logic/NotificationService.java
index e7df63e..9d30fde 100644
--- a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/logic/NotificationService.java
+++ b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/logic/NotificationService.java
@@ -1,20 +1,32 @@
package com.staffinghub.coding.challenges.retry.core.logic;
import com.staffinghub.coding.challenges.retry.core.entities.EmailNotification;
+import com.staffinghub.coding.challenges.retry.core.entities.EmailNotificationMessage;
import com.staffinghub.coding.challenges.retry.core.inbound.NotificationHandler;
-import com.staffinghub.coding.challenges.retry.core.outbound.NotificationSender;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.integration.core.MessagingTemplate;
+import org.springframework.messaging.PollableChannel;
+import org.springframework.messaging.support.MessageBuilder;
+@Slf4j
public class NotificationService implements NotificationHandler {
- private NotificationSender notificationSender;
+ private PollableChannel channel;
- public NotificationService(NotificationSender notificationSender) {
- this.notificationSender = notificationSender;
+ private final MessagingTemplate messagingTemplate = new MessagingTemplate();
+
+ public NotificationService(PollableChannel channel) {
+ this.channel = channel;
}
+ /** {@inheritDoc} */
@Override
public EmailNotification processEmailNotification(EmailNotification emailNotification) {
- notificationSender.sendEmail(emailNotification);
+ messagingTemplate.send(channel, MessageBuilder.withPayload(EmailNotificationMessage
+ .builder()
+ .emailNotification(emailNotification)
+ .build()).build());
+ log.info("Message queued: {}", emailNotification);
return emailNotification;
}
}
diff --git a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/outbound/NotificationSender.java b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/outbound/NotificationSender.java
index 2d8e001..3e07076 100644
--- a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/outbound/NotificationSender.java
+++ b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/core/outbound/NotificationSender.java
@@ -1,11 +1,22 @@
package com.staffinghub.coding.challenges.retry.core.outbound;
import com.staffinghub.coding.challenges.retry.core.entities.EmailNotification;
+import com.staffinghub.coding.challenges.retry.core.entities.EmailNotificationMessage;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
public interface NotificationSender {
+ /**
+ * Validates and submits a given emailNotification {@link EmailNotification} to send a new email
+ * @param emailNotification {@link EmailNotification}
+ */
void sendEmail(@Valid @NotNull EmailNotification emailNotification);
+
+ /**
+ * Re-queues a given emailNotificationMessage to retry the notification for a max of five tries using exponential back-off
+ * @param emailNotificationMessage {@link EmailNotificationMessage}
+ */
+ void notificationEvent(@Valid @NotNull EmailNotificationMessage emailNotificationMessage);
}
diff --git a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/inbound/EmailController.java b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/inbound/EmailController.java
index 71d78a2..9946ad7 100644
--- a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/inbound/EmailController.java
+++ b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/inbound/EmailController.java
@@ -18,6 +18,11 @@ public EmailController(NotificationHandler notificationHandler) {
this.notificationHandler = notificationHandler;
}
+ /**
+ * Creates a new email notification using the given emailNotification request {@link EmailNotification}
+ * @param emailNotification {@link EmailNotification} Email notification to request
+ * @return {@link ResponseEntity} The status of the notification request
+ */
@PostMapping
public ResponseEntity createEmailNotification(@RequestBody EmailNotification emailNotification) {
EmailNotification emailNotificationResult = notificationHandler.processEmailNotification(emailNotification);
diff --git a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/outbound/EmailNotificationSenderService.java b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/outbound/EmailNotificationSenderService.java
index 008cc6d..e007077 100644
--- a/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/outbound/EmailNotificationSenderService.java
+++ b/Backend/retry/src/main/java/com/staffinghub/coding/challenges/retry/outbound/EmailNotificationSenderService.java
@@ -1,28 +1,67 @@
package com.staffinghub.coding.challenges.retry.outbound;
import com.staffinghub.coding.challenges.retry.core.entities.EmailNotification;
+import com.staffinghub.coding.challenges.retry.core.entities.EmailNotificationMessage;
import com.staffinghub.coding.challenges.retry.core.outbound.NotificationSender;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
+import org.springframework.integration.core.MessagingTemplate;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.messaging.PollableChannel;
+import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+import static java.time.temporal.ChronoUnit.SECONDS;
@Service
@Validated
+@Slf4j
public class EmailNotificationSenderService implements NotificationSender {
private static final String SENDER_ADDRESS = "info@test.com";
private JavaMailSender mailSender;
- public EmailNotificationSenderService(JavaMailSender mailSender) {
+ private PollableChannel channel;
+
+ private final MessagingTemplate messagingTemplate = new MessagingTemplate();
+
+ public EmailNotificationSenderService(JavaMailSender mailSender, PollableChannel channel) {
this.mailSender = mailSender;
+ this.channel = channel;
+ }
+
+ /** {@inheritDoc} */
+ @Async
+ @EventListener
+ @Override
+ public void notificationEvent(EmailNotificationMessage emailNotificationMessage) {
+ log.info("Received notification by event for email {} in date {}.",
+ emailNotificationMessage,
+ emailNotificationMessage.getTimestamp());
+ try {
+ this.sendEmail(emailNotificationMessage.getEmailNotification());
+ } catch(Exception ex) {
+ if (emailNotificationMessage.getRetryAttempts() > 5) {
+ return;
+ }
+
+ emailNotificationMessage.setRetryAttempts(emailNotificationMessage.getRetryAttempts() + 1);
+ emailNotificationMessage.setDueTimestamp(LocalDateTime.now().plus(5 * (10^emailNotificationMessage.getRetryAttempts()), SECONDS));
+ messagingTemplate.send(channel, MessageBuilder.withPayload(emailNotificationMessage).build());
+ log.info("Message re-queued: {}", emailNotificationMessage.getEmailNotification());
+ }
+
}
+ /** {@inheritDoc} */
@Async
@Override
public void sendEmail(@Valid @NotNull EmailNotification emailNotification) {
@@ -34,6 +73,7 @@ public void sendEmail(@Valid @NotNull EmailNotification emailNotification) {
mailMessage.setText(emailNotification.getText());
mailSender.send(mailMessage);
+ log.info("Successfully sent: {}", emailNotification);
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to send email to recipient: %s", emailNotification.getRecipient()));
}
diff --git a/Backend/retry/src/main/resources/application.yml b/Backend/retry/src/main/resources/application.yml
index c415549..7dacfd6 100644
--- a/Backend/retry/src/main/resources/application.yml
+++ b/Backend/retry/src/main/resources/application.yml
@@ -11,3 +11,6 @@ spring:
starttls:
enable: false
required: false
+ data:
+ mongodb:
+ uri: mongodb://localhost:27017/db
diff --git a/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/core/logic/NotificationServiceIntegrationTest.java b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/core/logic/NotificationServiceIntegrationTest.java
new file mode 100644
index 0000000..7cd8ad9
--- /dev/null
+++ b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/core/logic/NotificationServiceIntegrationTest.java
@@ -0,0 +1,33 @@
+package com.staffinghub.coding.challenges.retry.core.logic;
+
+import com.staffinghub.coding.challenges.retry.core.entities.EmailNotification;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.messaging.PollableChannel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+
+// Instead of using the SpringBootTest annotation we can reduce the testing overhead by using the ContextConfiguration annotation while specifying the contexts we need to load in our tests
+@SpringBootTest
+public class NotificationServiceIntegrationTest {
+
+ @Autowired
+ private NotificationService notificationService;
+
+ @Test
+ public void submitEmailNotificationSuccessfully() {
+ EmailNotification notification = EmailNotification.builder().recipient("testRecipient@test.com").subject("Integration test subject").text("Test text").build();
+ EmailNotification response = notificationService.processEmailNotification(notification);
+
+ assertThat(response)
+ .usingRecursiveComparison()
+ .isEqualTo(notification);
+ }
+}
diff --git a/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/core/logic/NotificationServiceUnitTest.java b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/core/logic/NotificationServiceUnitTest.java
new file mode 100644
index 0000000..3a1c87d
--- /dev/null
+++ b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/core/logic/NotificationServiceUnitTest.java
@@ -0,0 +1,35 @@
+package com.staffinghub.coding.challenges.retry.core.logic;
+
+import com.staffinghub.coding.challenges.retry.core.entities.EmailNotification;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.messaging.PollableChannel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+
+@ExtendWith(MockitoExtension.class)
+public class NotificationServiceUnitTest {
+
+ @Mock
+ private PollableChannel channel;
+
+ @InjectMocks
+ private NotificationService notificationService;
+
+ @Test
+ public void submitEmailNotificationSuccessfully() {
+ doReturn(true).when(channel).send(any());
+
+ EmailNotification notification = EmailNotification.builder().recipient("testRecipient@test.com").subject("Test subject line").text("Test text").build();
+ EmailNotification response = notificationService.processEmailNotification(notification);
+
+ assertThat(response)
+ .usingRecursiveComparison()
+ .isEqualTo(notification);
+ }
+}
diff --git a/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/inbound/EmailControllerIntegrationTest.java b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/inbound/EmailControllerIntegrationTest.java
new file mode 100644
index 0000000..886bf23
--- /dev/null
+++ b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/inbound/EmailControllerIntegrationTest.java
@@ -0,0 +1,53 @@
+package com.staffinghub.coding.challenges.retry.inbound;
+
+import com.staffinghub.coding.challenges.retry.core.entities.EmailNotification;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import static com.staffinghub.coding.challenges.retry.utility.JsonHelpers.asJsonString;
+import static com.staffinghub.coding.challenges.retry.utility.JsonHelpers.asObject;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@ExtendWith(SpringExtension.class)
+@SpringBootTest
+@AutoConfigureMockMvc
+public class EmailControllerIntegrationTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ public void createEmailNotificationShouldReturnMessageFromService() throws Exception {
+
+ EmailNotification emailNotification = EmailNotification
+ .builder()
+ .recipient("test@retry.com")
+ .subject("Testing email notification")
+ .text("Some test text.")
+ .build();
+
+ MvcResult result = this.mockMvc.perform(
+ post("/v1/emails")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(asJsonString(emailNotification)))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andReturn();
+
+ EmailNotification resultObject = asObject(result.getResponse().getContentAsString(), EmailNotification.class);
+ assertThat(resultObject)
+ .usingRecursiveComparison()
+ .isEqualTo(emailNotification);
+ }
+}
diff --git a/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/inbound/EmailControllerUnitTest.java b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/inbound/EmailControllerUnitTest.java
new file mode 100644
index 0000000..a1391f2
--- /dev/null
+++ b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/inbound/EmailControllerUnitTest.java
@@ -0,0 +1,55 @@
+package com.staffinghub.coding.challenges.retry.inbound;
+
+import com.staffinghub.coding.challenges.retry.core.entities.EmailNotification;
+import com.staffinghub.coding.challenges.retry.core.inbound.NotificationHandler;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import static com.staffinghub.coding.challenges.retry.utility.JsonHelpers.asJsonString;
+import static com.staffinghub.coding.challenges.retry.utility.JsonHelpers.asObject;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(EmailController.class)
+public class EmailControllerUnitTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockBean
+ private NotificationHandler notificationHandler;
+
+ @Test
+ public void createEmailNotificationShouldReturnMessageFromService() throws Exception {
+
+ EmailNotification emailNotification = EmailNotification
+ .builder()
+ .recipient("test@retry.com")
+ .subject("Testing email notification")
+ .text("Some test text.")
+ .build();
+
+ when(notificationHandler.processEmailNotification(any())).thenReturn(emailNotification);
+ MvcResult result = this.mockMvc.perform(
+ post("/v1/emails")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(asJsonString(emailNotification)))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andReturn();
+
+ EmailNotification resultObject = asObject(result.getResponse().getContentAsString(), EmailNotification.class);
+ assertThat(resultObject)
+ .usingRecursiveComparison()
+ .isEqualTo(emailNotification);
+ }
+}
diff --git a/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/outbound/EmailNotificationSenderServiceIntegrationTest.java b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/outbound/EmailNotificationSenderServiceIntegrationTest.java
new file mode 100644
index 0000000..9926d02
--- /dev/null
+++ b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/outbound/EmailNotificationSenderServiceIntegrationTest.java
@@ -0,0 +1,32 @@
+package com.staffinghub.coding.challenges.retry.outbound;
+
+import com.staffinghub.coding.challenges.retry.core.entities.EmailNotification;
+import com.staffinghub.coding.challenges.retry.core.entities.EmailNotificationMessage;
+import com.staffinghub.coding.challenges.retry.core.outbound.NotificationSender;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+@ExtendWith(MockitoExtension.class)
+@SpringBootTest
+public class EmailNotificationSenderServiceIntegrationTest {
+
+ @Autowired
+ private NotificationSender emailNotificationSenderService;
+
+ private EmailNotification validEmail = EmailNotification.builder().recipient("testEmai@test.com").subject("Test subject").text("Test message").build();
+
+ @Test
+ public void sendValidEmail() {
+ assertDoesNotThrow(() -> emailNotificationSenderService.sendEmail(validEmail));
+ }
+
+ @Test
+ public void sendNotificationWithoutRetry() {
+ assertDoesNotThrow(() -> emailNotificationSenderService.notificationEvent(EmailNotificationMessage.builder().emailNotification(validEmail).build()));
+ }
+}
diff --git a/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/outbound/EmailNotificationSenderServiceUnitTest.java b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/outbound/EmailNotificationSenderServiceUnitTest.java
new file mode 100644
index 0000000..bf79b50
--- /dev/null
+++ b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/outbound/EmailNotificationSenderServiceUnitTest.java
@@ -0,0 +1,62 @@
+package com.staffinghub.coding.challenges.retry.outbound;
+
+import com.staffinghub.coding.challenges.retry.core.entities.EmailNotification;
+import com.staffinghub.coding.challenges.retry.core.entities.EmailNotificationMessage;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.messaging.PollableChannel;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+
+@ExtendWith(MockitoExtension.class)
+public class EmailNotificationSenderServiceUnitTest {
+
+ @Mock
+ private JavaMailSender mailSender;
+
+ @Mock
+ private PollableChannel channel;
+
+ @InjectMocks
+ private EmailNotificationSenderService emailNotificationSenderService;
+
+ private EmailNotification validEmail = EmailNotification.builder().recipient("testEmai@test.com").subject("Test subject").text("Test message").build();
+
+ @Test
+ public void sendValidEmail() {
+ assertDoesNotThrow(() -> emailNotificationSenderService.sendEmail(validEmail));
+ }
+
+ @Test
+ public void sendValidEmailServiceFailure() {
+ doThrow(RuntimeException.class).when(mailSender).send(any(SimpleMailMessage.class));
+ assertThrows(RuntimeException.class, () -> emailNotificationSenderService.sendEmail(validEmail));
+ }
+
+ @Test
+ public void sendNotificationWithoutRetry() {
+ emailNotificationSenderService.notificationEvent(EmailNotificationMessage.builder().emailNotification(validEmail).build());
+ }
+
+ @ParameterizedTest(name = "Retry notification system: retry attempt number: {0}")
+ @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6})
+ public void sendNotificationWithExponentialFallback(int retryAttempt) {
+ if(retryAttempt <= 5) {
+ doReturn(true).when(channel).send(any());
+ }
+ doThrow(RuntimeException.class).when(mailSender).send(any(SimpleMailMessage.class));
+ EmailNotificationMessage validEmailNotification = EmailNotificationMessage.builder().retryAttempts(retryAttempt).emailNotification(validEmail).build();
+ emailNotificationSenderService.notificationEvent(validEmailNotification);
+ }
+}
diff --git a/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/utility/JsonHelpers.java b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/utility/JsonHelpers.java
new file mode 100644
index 0000000..f5502b7
--- /dev/null
+++ b/Backend/retry/src/test/java/com/staffinghub/coding/challenges/retry/utility/JsonHelpers.java
@@ -0,0 +1,34 @@
+package com.staffinghub.coding.challenges.retry.utility;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class JsonHelpers {
+
+ /**
+ * Converts a given object into a JSON string (serializes object)
+ * @param obj {@link Object}
+ * @return {@link String}
+ */
+ public static String asJsonString(final Object obj) {
+ try {
+ return new ObjectMapper().writeValueAsString(obj);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Converts a given JSON string into an object of the given class (de-serializes string)
+ * @param jsonString {@link String} JSON string to de-serialize
+ * @param var {@link Class} The class of the object to convert the string into
+ * @return {@link T} An object of the given type
+ * @param
+ */
+ public static T asObject(final String jsonString, Class var) {
+ try {
+ return new ObjectMapper().readValue(jsonString, var);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}