From 0f997e913943861932833067645d989fb5772838 Mon Sep 17 00:00:00 2001 From: Mikel Alejo Date: Fri, 22 Sep 2023 09:14:33 +0200 Subject: [PATCH] feature: tests for the email proecessor (#2165) The goal is to have the email processor fully tested. RHCLOUD-28334 --- .../SystemEndpointTypeProcessor.java | 9 +- .../connector/dto/RecipientSettings.java | 36 ++ .../processors/email/EmailProcessorTest.java | 400 ++++++++++++++++++ 3 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 engine/src/test/java/com/redhat/cloud/notifications/processors/email/EmailProcessorTest.java diff --git a/engine/src/main/java/com/redhat/cloud/notifications/processors/SystemEndpointTypeProcessor.java b/engine/src/main/java/com/redhat/cloud/notifications/processors/SystemEndpointTypeProcessor.java index 56d3fef81f..270cc6e72c 100644 --- a/engine/src/main/java/com/redhat/cloud/notifications/processors/SystemEndpointTypeProcessor.java +++ b/engine/src/main/java/com/redhat/cloud/notifications/processors/SystemEndpointTypeProcessor.java @@ -61,7 +61,14 @@ protected Set extractRecipientSettings(final Event event, fin ).collect(Collectors.toSet()); } - protected Set extractAndTransformRecipientSettings(final Event event, final List endpoints) { + /** + * Extracts the recipient settings from the event and the endpoints and + * transforms them to a DTO in order to send them to the email connector. + * @param event the event to extract the recipient settings from. + * @param endpoints the endpoints to extract the recipient settings from. + * @return a set of recipient settings DTOs. + */ + public Set extractAndTransformRecipientSettings(final Event event, final List endpoints) { final Set recipientSettings = this.extractRecipientSettings(event, endpoints); return recipientSettings diff --git a/engine/src/main/java/com/redhat/cloud/notifications/processors/email/connector/dto/RecipientSettings.java b/engine/src/main/java/com/redhat/cloud/notifications/processors/email/connector/dto/RecipientSettings.java index 150b4e3e1a..02595b92c2 100644 --- a/engine/src/main/java/com/redhat/cloud/notifications/processors/email/connector/dto/RecipientSettings.java +++ b/engine/src/main/java/com/redhat/cloud/notifications/processors/email/connector/dto/RecipientSettings.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; +import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -18,6 +19,13 @@ public class RecipientSettings { private final UUID groupUUID; private final Set users; + public RecipientSettings(final boolean adminsOnly, final boolean ignoreUserPreferences, final UUID groupUUID, final Set users) { + this.adminsOnly = adminsOnly; + this.ignoreUserPreferences = ignoreUserPreferences; + this.groupUUID = groupUUID; + this.users = users; + } + public RecipientSettings(final com.redhat.cloud.notifications.recipients.RecipientSettings recipientSettings) { this.adminsOnly = recipientSettings.isOnlyAdmins(); this.ignoreUserPreferences = recipientSettings.isIgnoreUserPreferences(); @@ -40,4 +48,32 @@ public UUID getGroupUUID() { public Set getUsers() { return this.users; } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + + if (o == null || this.getClass() != o.getClass()) { + return false; + } + + final RecipientSettings that = (RecipientSettings) o; + + return this.adminsOnly == that.adminsOnly + && this.ignoreUserPreferences == that.ignoreUserPreferences + && Objects.equals(this.groupUUID, that.groupUUID) + && Objects.equals(this.users, that.users); + } + + @Override + public int hashCode() { + return Objects.hash( + this.adminsOnly ? 1 : 0, + this.ignoreUserPreferences ? 1 : 0, + this.groupUUID != null ? this.groupUUID.hashCode() : 0, + this.users != null ? this.users.hashCode() : 0 + ); + } } diff --git a/engine/src/test/java/com/redhat/cloud/notifications/processors/email/EmailProcessorTest.java b/engine/src/test/java/com/redhat/cloud/notifications/processors/email/EmailProcessorTest.java new file mode 100644 index 0000000000..e11ae7d47f --- /dev/null +++ b/engine/src/test/java/com/redhat/cloud/notifications/processors/email/EmailProcessorTest.java @@ -0,0 +1,400 @@ +package com.redhat.cloud.notifications.processors.email; + +import com.redhat.cloud.event.core.v1.Recipients; +import com.redhat.cloud.notifications.db.repositories.EmailSubscriptionRepository; +import com.redhat.cloud.notifications.db.repositories.EndpointRepository; +import com.redhat.cloud.notifications.db.repositories.TemplateRepository; +import com.redhat.cloud.notifications.events.EventWrapper; +import com.redhat.cloud.notifications.events.EventWrapperCloudEvent; +import com.redhat.cloud.notifications.models.Application; +import com.redhat.cloud.notifications.models.Bundle; +import com.redhat.cloud.notifications.models.EmailSubscriptionType; +import com.redhat.cloud.notifications.models.Endpoint; +import com.redhat.cloud.notifications.models.EndpointType; +import com.redhat.cloud.notifications.models.Event; +import com.redhat.cloud.notifications.models.EventType; +import com.redhat.cloud.notifications.models.EventTypeKeyFqn; +import com.redhat.cloud.notifications.models.InstantEmailTemplate; +import com.redhat.cloud.notifications.models.NotificationsConsoleCloudEvent; +import com.redhat.cloud.notifications.models.SystemSubscriptionProperties; +import com.redhat.cloud.notifications.models.Template; +import com.redhat.cloud.notifications.processors.ConnectorSender; +import com.redhat.cloud.notifications.processors.email.connector.dto.RecipientSettings; +import com.redhat.cloud.notifications.templates.TemplateService; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import io.vertx.core.json.JsonObject; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@QuarkusTest +public class EmailProcessorTest { + @InjectMock + ConnectorSender connectorSender; + + @Inject + EmailProcessor emailProcessor; + + @InjectMock + EmailSubscriptionRepository emailSubscriptionRepository; + + @InjectMock + EndpointRepository endpointRepository; + + @InjectMock + TemplateRepository templateRepository; + + @InjectMock + TemplateService templateService; + + /** + * Creates a stub event with just the necessary elements to satisfy what is + * required in the tests of this class. + */ + private Event setUpStubEvent() { + final String[] users = {"foo", "bar", "baz"}; + + final Recipients recipients = new Recipients(); + recipients.setIgnoreUserPreferences(true); + recipients.setOnlyAdmins(true); + recipients.setUsers(users); + + final NotificationsConsoleCloudEvent notificationsConsoleCloudEvent = Mockito.mock(NotificationsConsoleCloudEvent.class); + // The type is required for the event wrapper. + notificationsConsoleCloudEvent.setType(EndpointType.EMAIL_SUBSCRIPTION.toString().toLowerCase()); + // We need to mock the result because there is no setter which allows + // us to set recipients. + Mockito.when(notificationsConsoleCloudEvent.getRecipients()).thenReturn(Optional.of(recipients)); + + final EventWrapper eventWrapper = new EventWrapperCloudEvent(notificationsConsoleCloudEvent); + + final Bundle bundle = new Bundle(); + bundle.setName("email-processor-test-bundle-name"); + final Application application = new Application(); + application.setBundle(bundle); + application.setName("email-processor-test-application-name"); + + final EventType eventType = new EventType(); + eventType.setApplication(application); + eventType.setName("email-processor-test-event-type"); + + final Event event = new Event(); + event.setId(UUID.randomUUID()); + event.setEventWrapper(eventWrapper); + event.setEventType(eventType); + event.setOrgId("email-processor-test-event-type-org-id"); + + return event; + } + + /** + * Creates three very specific stub endpoints. The goal of having these + * very specific endpoints created is to satisfy what is required in the + * {@link EmailProcessorTest#testExtractAndTransformRecipientSettings()} + * test. + * @return
    + *
  • A first endpoint with the "ignore user preferences" flag set to + * true, and the "admin only" flag set to false, and with a group UUID set. + *
  • + *
  • A second endpoint with both the "ignore user preferences" and "admin + * only" flags set to true, and with a group UUID set.
  • + *
  • A third endpoint without a group UUID set, and both "ignore user + * preferences" and "admin only" flags set to false./li> + *
+ */ + private List setUpStubEndpoints() { + // Create the first properties and the first endpoint. + final SystemSubscriptionProperties systemSubscriptionProperties = new SystemSubscriptionProperties(); + systemSubscriptionProperties.setGroupId(UUID.randomUUID()); + systemSubscriptionProperties.setIgnorePreferences(true); + systemSubscriptionProperties.setOnlyAdmins(false); + + final Endpoint endpoint = new Endpoint(); + endpoint.setProperties(systemSubscriptionProperties); + + // Create the second properties and the second endpoint. + final SystemSubscriptionProperties systemSubscriptionProperties2 = new SystemSubscriptionProperties(); + systemSubscriptionProperties2.setGroupId(UUID.randomUUID()); + systemSubscriptionProperties2.setIgnorePreferences(true); + systemSubscriptionProperties2.setOnlyAdmins(true); + + final Endpoint endpoint2 = new Endpoint(); + endpoint2.setProperties(systemSubscriptionProperties2); + + // Create the third properties and the third endpoint. + final SystemSubscriptionProperties systemSubscriptionProperties3 = new SystemSubscriptionProperties(); + systemSubscriptionProperties3.setIgnorePreferences(false); + systemSubscriptionProperties3.setOnlyAdmins(false); + + final Endpoint endpoint3 = new Endpoint(); + endpoint3.setProperties(systemSubscriptionProperties3); + + return List.of(endpoint, endpoint2, endpoint3); + } + + /** + * The common elements to all tests that need to be taken care of. In this + * particular case: + * + *
    + *
  • The {@link TemplateRepository#isEmailAggregationSupported(String, String, List)} + * always returns {code false}, because we do not want to hit the + * database, and the goal of this tests class is not to test that class' + * function. + *
  • + *
+ */ + @BeforeEach + void beforeEachTest() { + Mockito.when(this.templateRepository.isEmailAggregationSupported(Mockito.any(UUID.class))).thenReturn(false); + } + + /** + * Tests that the function under test is able to extract the recipient + * settings from the stubbed event and endpoints' list. + */ + @Test + void testExtractAndTransformRecipientSettings() { + final Event event = this.setUpStubEvent(); + final List endpoints = this.setUpStubEndpoints(); + + // Call the function under test. + final Set resultSet = this.emailProcessor.extractAndTransformRecipientSettings(event, endpoints); + + // Assert that the generated recipient settings contain the right + // values. + for (final RecipientSettings recipientSettings : resultSet) { + // When the users are empty we know that the recipient settings + // were generated from the endpoints. Otherwise, they were + // generated from the event. + if (recipientSettings.getUsers().isEmpty()) { + // When the group ID is null, we know we have the third + // endpoint in our hands, which should have both flags set to + // false. + if (recipientSettings.getGroupUUID() == null) { + Assertions.assertFalse(recipientSettings.isAdminsOnly(), String.format("the created recipient settings should have the \"admins only\" flag set to false: %s", recipientSettings)); + Assertions.assertFalse(recipientSettings.isIgnoreUserPreferences(), String.format("the created recipient settings should have the \"ignore user preferences\" flag set to false: %s", recipientSettings)); + } else { + // In this case we need to find which endpoint the + // recipient settings correspond to, in order to compare + // the flags properly. + final Optional endpoint = endpoints.stream() + .filter(e -> recipientSettings.getGroupUUID().equals(e.getProperties(SystemSubscriptionProperties.class).getGroupId())) + .findAny(); + + if (endpoint.isEmpty()) { + Assertions.fail("unable to find the endpoint with the specified group"); + } + + final SystemSubscriptionProperties properties = endpoint.get().getProperties(SystemSubscriptionProperties.class); + + Assertions.assertEquals(properties.isIgnorePreferences(), recipientSettings.isIgnoreUserPreferences(), "the \"ignore user preferences\" flag in the recipient settings does not match the same flag in the endpoint's properties"); + Assertions.assertEquals(properties.isOnlyAdmins(), recipientSettings.isAdminsOnly(), "the \"admins only\" flag in the recipient settings does not match the same flag in the endpoint's properties"); + } + } else { + Assertions.assertTrue(recipientSettings.isAdminsOnly(), "the \"admins only\" flag in the recipient settings does not match the same flag from the event"); + Assertions.assertTrue(recipientSettings.isIgnoreUserPreferences(), "the \"ignore user preferences\" flag in the recipient settings does not match the same flag from the event"); + } + } + } + + /** + * Tests that when there is no associated email template for the given + * event, then the processor ignores the event. + */ + @Test + void testMissingEmailTemplate() { + // Prepare the required stubs. + final Event event = this.setUpStubEvent(); + final List endpoints = this.setUpStubEndpoints(); + + // Simulate that there is no email template available for the event. + Mockito.when(this.templateRepository.findInstantEmailTemplate(event.getEventType().getId())).thenReturn(Optional.empty()); + + // Call the processor under test. + this.emailProcessor.process(event, endpoints); + + // Verify that the processor returned without calling any further + // dependencies in the code. + Mockito.verify(this.templateService, Mockito.times(0)).compileTemplate(Mockito.anyString(), Mockito.anyString()); + Mockito.verify(this.templateService, Mockito.times(0)).renderTemplate(Mockito.any(), Mockito.any(TemplateInstance.class)); + Mockito.verify(this.endpointRepository, Mockito.times(0)).getOrCreateDefaultSystemSubscription(Mockito.anyString(), Mockito.anyString(), Mockito.eq(EndpointType.EMAIL_SUBSCRIPTION)); + Mockito.verify(this.connectorSender, Mockito.times(0)).send(Mockito.any(Event.class), Mockito.any(Endpoint.class), Mockito.any(JsonObject.class)); + } + + /** + * Tests that when there are no subscribers associated to the event type, + * then the processor ignores the event. + */ + @Test + void testMissingSubscribers() { + // Prepare the required stubs. + final Event event = this.setUpStubEvent(); + final List endpoints = this.setUpStubEndpoints(); + + // Create the stub templates for the instant email template that we + // will simulate that is returned from the template repository. + final String subjectData = "test-missing-subscribers-subject-data"; + final String bodyData = "test-missing-subscribers-body-data"; + + final Template subjectTemplate = new Template(); + subjectTemplate.setData(subjectData); + + final Template bodyTemplate = new Template(); + bodyTemplate.setData(bodyData); + + final InstantEmailTemplate instantEmailTemplate = new InstantEmailTemplate(); + instantEmailTemplate.setSubjectTemplate(subjectTemplate); + instantEmailTemplate.setBodyTemplate(bodyTemplate); + + Mockito.when(this.templateRepository.findInstantEmailTemplate(event.getEventType().getId())).thenReturn(Optional.of(instantEmailTemplate)); + + // Mock the template instances that will be returned from the compiling + // operation. + final TemplateInstance subjectTemplateInstance = Mockito.mock(TemplateInstance.class); + final TemplateInstance bodyTemplateInstance = Mockito.mock(TemplateInstance.class); + Mockito.when(this.templateService.compileTemplate(subjectData, "subject")).thenReturn(subjectTemplateInstance); + Mockito.when(this.templateService.compileTemplate(bodyData, "body")).thenReturn(bodyTemplateInstance); + + // Mock the rendered contents that will be returned from the rendering + // operation. + final String stubbedRenderedSubject = "test-missing-subscribers-rendered-subject"; + final String stubbedRenderedBody = "test-missing-subscribers-rendered-body"; + Mockito.when(this.templateService.renderTemplate(event.getEventWrapper().getEvent(), subjectTemplateInstance)).thenReturn(stubbedRenderedSubject); + Mockito.when(this.templateService.renderTemplate(event.getEventWrapper().getEvent(), bodyTemplateInstance)).thenReturn(stubbedRenderedBody); + + // Do not return any subscribers for this test. + Mockito.when(this.emailSubscriptionRepository.getSubscribersByEventType(event.getOrgId(), event.getEventType().getId(), EmailSubscriptionType.INSTANT)).thenReturn(List.of()); + + // Call the processor under test. + this.emailProcessor.process(event, endpoints); + + // Verify that the compilation functions were called. + Mockito.verify(this.templateService, Mockito.times(1)).compileTemplate(subjectData, "subject"); + Mockito.verify(this.templateService, Mockito.times(1)).compileTemplate(bodyData, "body"); + + // Verify that the rendering functions were called. + Mockito.verify(this.templateService, Mockito.times(1)).renderTemplate(event.getEventWrapper().getEvent(), subjectTemplateInstance); + Mockito.verify(this.templateService, Mockito.times(1)).renderTemplate(event.getEventWrapper().getEvent(), bodyTemplateInstance); + + // Verify that, since there were no subscribers to notify, the event + // was ignored. + Mockito.verify(this.endpointRepository, Mockito.times(0)).getOrCreateDefaultSystemSubscription(Mockito.anyString(), Mockito.anyString(), Mockito.eq(EndpointType.EMAIL_SUBSCRIPTION)); + Mockito.verify(this.connectorSender, Mockito.times(0)).send(Mockito.any(Event.class), Mockito.any(Endpoint.class), Mockito.any(JsonObject.class)); + } + + /** + * Tests that when everything goes right, then the connector receives the + * correct payload. + */ + @Test + void testSuccess() { + // Prepare the required stubs. + final Event event = this.setUpStubEvent(); + final List endpoints = this.setUpStubEndpoints(); + + // Create the stub templates for the instant email template that we + // will simulate that is returned from the template repository. + final String subjectData = "test-missing-subscribers-subject-data"; + final String bodyData = "test-missing-subscribers-body-data"; + + final Template subjectTemplate = new Template(); + subjectTemplate.setData(subjectData); + + final Template bodyTemplate = new Template(); + bodyTemplate.setData(bodyData); + + final InstantEmailTemplate instantEmailTemplate = new InstantEmailTemplate(); + instantEmailTemplate.setSubjectTemplate(subjectTemplate); + instantEmailTemplate.setBodyTemplate(bodyTemplate); + + Mockito.when(this.templateRepository.findInstantEmailTemplate(event.getEventType().getId())).thenReturn(Optional.of(instantEmailTemplate)); + + // Mock the template instances that will be returned from the compiling + // operation. + final TemplateInstance subjectTemplateInstance = Mockito.mock(TemplateInstance.class); + final TemplateInstance bodyTemplateInstance = Mockito.mock(TemplateInstance.class); + Mockito.when(this.templateService.compileTemplate(subjectData, "subject")).thenReturn(subjectTemplateInstance); + Mockito.when(this.templateService.compileTemplate(bodyData, "body")).thenReturn(bodyTemplateInstance); + + // Mock the rendered contents that will be returned from the rendering + // operation. + final String stubbedRenderedSubject = "test-missing-subscribers-rendered-subject"; + final String stubbedRenderedBody = "test-missing-subscribers-rendered-body"; + Mockito.when(this.templateService.renderTemplate(event.getEventWrapper().getEvent(), subjectTemplateInstance)).thenReturn(stubbedRenderedSubject); + Mockito.when(this.templateService.renderTemplate(event.getEventWrapper().getEvent(), bodyTemplateInstance)).thenReturn(stubbedRenderedBody); + + // Mock a list of subscribers that simulate the ones that should be + // notified for the event. + final List subscribers = List.of("subscriber-a", "subscriber-b", "subscriber-c"); + Mockito.when(this.emailSubscriptionRepository.getSubscribersByEventType(event.getOrgId(), event.getEventType().getId(), EmailSubscriptionType.INSTANT)).thenReturn(subscribers); + + // Mock the endpoint that should get pulled from the database using + // the endpoint repository. + final Endpoint endpoint = new Endpoint(); + endpoint.setId(UUID.randomUUID()); + Mockito.when(this.endpointRepository.getOrCreateDefaultSystemSubscription(event.getAccountId(), event.getOrgId(), EndpointType.EMAIL_SUBSCRIPTION)).thenReturn(endpoint); + + // Call the processor under test. + this.emailProcessor.process(event, endpoints); + + // Verify that the compilation functions were called. + Mockito.verify(this.templateService, Mockito.times(1)).compileTemplate(subjectData, "subject"); + Mockito.verify(this.templateService, Mockito.times(1)).compileTemplate(bodyData, "body"); + + // Verify that the rendering functions were called. + Mockito.verify(this.templateService, Mockito.times(1)).renderTemplate(event.getEventWrapper().getEvent(), subjectTemplateInstance); + Mockito.verify(this.templateService, Mockito.times(1)).renderTemplate(event.getEventWrapper().getEvent(), bodyTemplateInstance); + + // Verify that the endpoint repository was called to fetch the result + // endpoint. + Mockito.verify(this.endpointRepository, Mockito.times(1)).getOrCreateDefaultSystemSubscription(event.getAccountId(), event.getOrgId(), EndpointType.EMAIL_SUBSCRIPTION); + + // Verify that the connector was called with the right parameters. + final ArgumentCaptor capturedEvent = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor capturedEndpoint = ArgumentCaptor.forClass(Endpoint.class); + final ArgumentCaptor capturedPayload = ArgumentCaptor.forClass(JsonObject.class); + Mockito.verify(this.connectorSender, Mockito.times(1)).send(capturedEvent.capture(), capturedEndpoint.capture(), capturedPayload.capture()); + + Assertions.assertEquals(event, capturedEvent.getValue(), "the captured event does not match with the stubbed one"); + Assertions.assertEquals(endpoint, capturedEndpoint.getValue(), "the captured endpoint does not match with the stubbed one"); + + final JsonObject payload = capturedPayload.getValue(); + final String resultEmailBody = payload.getString("email_body"); + final String resultEmailSubject = payload.getString("email_subject"); + final String resultOrgId = payload.getString("orgId"); + final List resultSubscribers = payload.getJsonArray("subscribers").stream().map(String.class::cast).toList(); + final Set resultRecipientSettings = payload.getJsonArray("recipient_settings") + .stream() + .map(JsonObject.class::cast) + .map(jsonRs -> { + final String groupUUID = jsonRs.getString("group_uuid"); + + return new RecipientSettings( + jsonRs.getBoolean("admins_only"), + jsonRs.getBoolean("ignore_user_preferences"), + (groupUUID == null) ? null : UUID.fromString(groupUUID), + jsonRs.getJsonArray("users").stream().map(String.class::cast).collect(Collectors.toSet()) + ); + }).collect(Collectors.toSet()); + + Assertions.assertEquals(stubbedRenderedBody, resultEmailBody, "the rendered email's body from the email notification does not match the stubbed email body"); + Assertions.assertEquals(stubbedRenderedSubject, resultEmailSubject, "the rendered email's subject from the email notification does not match the stubbed email subject"); + Assertions.assertEquals(event.getOrgId(), resultOrgId, "the organization ID from the email notification does not match the one set in the stubbed event"); + Assertions.assertIterableEquals(subscribers, resultSubscribers, "the subscribers set in the email notification do not match the stubbed ones"); + + final Set recipientSettings = this.emailProcessor.extractAndTransformRecipientSettings(event, endpoints); + Assertions.assertIterableEquals(recipientSettings, resultRecipientSettings, "the recipient settings set in the email notification do not match the stubbed ones"); + } +}