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); + } + } +}